0
0
Fork 0
mirror of https://github.com/ryujinx-mirror/ryujinx.git synced 2024-12-22 17:25:45 +00:00

Vulkan backend (#2518)

* WIP Vulkan implementation

* No need to initialize attributes on the SPIR-V backend anymore

* Allow multithreading shaderc and vkCreateShaderModule

You'll only really see the benefit here with threaded-gal or parallel shader cache compile.

Fix shaderc multithreaded changes

Thread safety for shaderc Options constructor

Dunno how they managed to make a constructor not thread safe, but you do you. May avoid some freezes.

* Support multiple levels/layers for blit.

Fixes MK8D when scaled, maybe a few other games. AMD software "safe" blit not supported right now.

* TextureStorage should hold a ref of the foreign storage, otherwise it might be freed while in use

* New depth-stencil blit method for AMD

* Workaround for AMD driver bug

* Fix some tessellation related issues (still doesn't work?)

* Submit command buffer before Texture GetData. (UE4 fix)

* DrawTexture support

* Fix BGRA on OpenGL backend

* Fix rebase build break

* Support format aliasing on SetImage

* Fix uniform buffers being lost when bindings are out of order

* Fix storage buffers being lost when bindings are out of order

(also avoid allocations when changing bindings)

* Use current command buffer for unscaled copy (perf)

Avoids flushing commands and renting a command buffer when fulfilling copy dependencies and when games do unscaled copies.

* Update to .net6

* Update Silk.NET to version 2.10.1

Somehow, massive performance boost. Seems like their vtable for looking up vulkan methods was really slow before.

* Fix PrimitivesGenerated query, disable Transform Feedback queries for now

Lets Splatoon 2 work on nvidia. (mostly)

* Update counter queue to be similar to the OGL one

Fixes softlocks when games had to flush counters.

* Don't throw when ending conditional rendering for now

This should be re-enabled when conditional rendering is enabled on nvidia etc.

* Update findMSB/findLSB to match master's instruction enum

* Fix triangle overlay on SMO, Captain Toad, maybe others?

* Don't make Intel Mesa pay for Intel Windows bugs

* Fix samplers with MinFilter Linear or Nearest (fixes New Super Mario Bros U Deluxe black borders)

* Update Spv.Generator

* Add alpha test emulation on shader (but no shader specialisation yet...)

* Fix R4G4B4A4Unorm texture format permutation

* Validation layers should be enabled for any log level other than None

* Add barriers around vkCmdCopyImage

Write->Read barrier for src image (we want to wait for a write to read it)
Write->Read barrier for dst image (we want to wait for the copy to complete before use)

* Be a bit more careful with texture access flags, since it can be used for anything

* Device local mapping for all buffers

May avoid issues with drivers with NVIDIA on linux/older gpus on windows when using large buffers (?)
Also some performance things and fixes issues with opengl games loading textures weird.

* Cleanup, disable device local buffers for now.

* Add single queue support

Multiqueue seems to be a bit more responsive on NVIDIA. Should fix texture flush on intel. AMD has been forced to single queue for an experiment.

* Fix some validation errors around extended dynamic state

* Remove Intel bug workaround, it was fixed on the latest driver

* Use circular queue for checking consumption on command buffers

Speeds up games that spam command buffers a little. Avoids checking multiple command buffers if multiple are active at once.

* Use SupportBufferUpdater, add single layer flush

* Fix counter queue leak when game decides to use host conditional rendering

* Force device local storage for textures (fixes linux performance)

* Port #3019

* Insert barriers around vkCmdBlitImage (may fix some amd flicker)

* Fix transform feedback on Intel, gl_Position feedback and clears to inexistent depth buffers

* Don't pause transform feedback for multi draw

* Fix draw outside of render pass and missing capability

* Workaround for wrong last attribute on AMD (affects FFVII, STRIKERS1945, probably more)

* Better workaround for AMD vertex buffer size alignment issue

* More instructions + fixes on SPIR-V backend

* Allow custom aspect ratio on Vulkan

* Correct GTK UI status bar positions

* SPIR-V: Functions must always end with a return

* SPIR-V: Fix ImageQuerySizeLod

* SPIR-V: Set DepthReplacing execution mode when FragDepth is modified

* SPIR-V: Implement LoopContinue IR instruction

* SPIR-V: Geometry shader support

* SPIR-V: Use correct binding number on storage buffers array

* Reduce allocations for Spir-v serialization

Passes BinaryWriter instead of the stream to Write and WriteOperand

- Removes creation of BinaryWriter for each instruction
- Removes allocations for literal string

* Some optimizations to Spv.Generator

- Dictionary for lookups of type declarations, constants, extinst
- LiteralInteger internal data format -> ushort
- Deterministic HashCode implementation to avoid spirv result not being the same between runs
- Inline operand list instead of List<T>, falls back to array if many operands. (large performance boost)

TODO: improve instruction allocation, structured program creator, ssa?

* Pool Spv.Generator resources, cache delegates, spv opts

- Pools for Instructions and LiteralIntegers. Can be passed in when creating the generator module.
  - NewInstruction is called instead of new Instruction()
  - Ryujinx SpirvGenerator passes in some pools that are static. The idea is for these to be shared between threads eventually.
- Estimate code size when creating the output MemoryStream
- LiteralInteger pools using ThreadStatic pools that are initialized before and after creation... not sure of a better way since the way these are created is via implicit cast.

Also, cache delegates for Spv.Generator for functions that are passed around to GenerateBinary etc, since passing the function raw creates a delegate on each call.

TODO: update python spv cs generator to make the coregrammar with NewInstruction and the `params` overloads.

* LocalDefMap for Ssa Rewriter

Rather than allocating a large array of all registers for each block in the shader, allocate one array of all registers and clear it between blocks. Reduces allocations in the shader translator.

* SPIR-V: Transform feedback support

* SPIR-V: Fragment shader interlock support (and image coherency)

* SPIR-V: Add early fragment tests support

* SPIR-V: Implement SwizzleAdd, add missing Triangles ExecutionMode for geometry shaders, remove SamplerType field from TextureMeta

* Don't pass depth clip state right now (fix decals)

Explicitly disabling it is incorrect. OpenGL currently automatically disables based on depth clamp, which is the behaviour if this state is omitted.

* Multisampling support

* Multisampling: Use resolve if src samples count > dst samples count

* Multisampling: We can only resolve for unscaled copies

* SPIR-V: Only add FSI exec mode if used.

* SPIR-V: Use ConstantComposite for Texture Offset Vector

Fixes a bunch of freezes with SPIR-V on AMD hardware, and validation errors. Note: Obviously assumes input offsets are constant, which they currently are.

* SPIR-V: Don't OpReturn if we already OpExit'ed

Fixes spir-v parse failure and stack smashing in RADV (obviously you still need bolist)

* SPIR-V: Only use input attribute type for input attributes

Output vertex attributes should always be of type float.

* Multithreaded Pipeline Compilation

* Address some feedback

* Make this 32

* Update topology with GpuAccessorState

* Cleanup for merge (note: disables spir-v)

* Make more robust to shader compilation failure

- Don't freeze when GLSL compilation fails
- Background SPIR-V pipeline compile failure results in skipped draws, similar to GLSL compilation failure.

* Fix Multisampling

* Only update fragment scale count if a vertex texture needs a scale.

Fixes a performance regression introduced by texture scaling in the vertex stage where support buffer updates would be very frequent, even at 1x, if any textures were used on the vertex stage.

This check doesn't exactly look cheap (a flag in the shader stage would probably be preferred), but it is much cheaper than uploading scales in both vulkan and opengl, so it will do for now.

* Use a bitmap to do granular tracking for buffer uploads.

This path is only taken if the much faster check of "is the buffer rented at all" is triggered, so it doesn't actually end up costing too much, and the time saved by not ending render passes (and on gpu for not waiting on barriers) is probably helpful.

Avoids ending render passes to update buffer data (not all the time)
- 140-180 to 35-45 in SMO metro kingdom (these updates are in the UI)
- Very variable 60-150(!) to 16-25 in mario kart 8 (these updates are in the UI)

As well as allowing more data to be preloaded persistently, this will also allow more data to be loaded in the preload buffer, which should be faster as it doesn't need to insert barriers between draws. (and on tbdr, does not need to flush and reload tile memory)

Improves performance in GPU limited scenarios. Should notably improve performance on TBDR gpus. Still a lot more to do here.

* Copy query results after RP ends, rather than ending to copy

We need to end the render pass to get the data (submit command buffer) anyways...

Reduces render passes created in games that use queries.

* Rework Query stuff a bit to avoid render pass end

Tries to reset returned queries in background when possible, rather than ending the render pass.

Still ends render pass when resetting a counter after draws, but maybe that can be solved too. (by just pulling an empty object off the pool?)

* Remove unnecessary lines

Was for testing

* Fix validation error for query reset

Need to think of a better way to do this.

* SPIR-V: Fix SwizzleAdd and some validation errors

* SPIR-V: Implement attribute indexing and StoreAttribute

* SPIR-V: Fix TextureSize for MS and Buffer sampler types

* Fix relaunch issues

* SPIR-V: Implement LogicalExclusiveOr

* SPIR-V: Constant buffer indexing support

* Ignore unsupported attributes rather than throwing (matches current GLSL behaviour)

* SPIR-V: Implement tessellation support

* SPIR-V: Geometry shader passthrough support

* SPIR-V: Implement StoreShader8/16 and StoreStorage8/16

* SPIR-V: Resolution scale support and fix TextureSample multisample with LOD bug

* SPIR-V: Fix field index for scale count

* SPIR-V: Fix another case of wrong field index

* SPIRV/GLSL: More scaling related fixes

* SPIR-V: Fix ImageLoad CompositeExtract component type

* SPIR-V: Workaround for Intel FrontFacing bug

* Enable SPIR-V backend by default

* Allow null samplers (samplers are not required when only using texelFetch to access the texture)

* Fix some validation errors related to texel block view usage flag and invalid image barrier base level

* Use explicit subgroup size if we can (might fix some block flickering on AMD)

* Take componentMask and scissor into account when clearing framebuffer attachments

* Add missing barriers around CmdFillBuffer (fixes Monster Hunter Rise flickering on NVIDIA)

* Use ClampToEdge for Clamp sampler address mode on Vulkan (fixes Hollow Knight)

Clamp is unsupported on Vulkan, but ClampToEdge behaves almost the same. ClampToBorder on the other hand (which was being used before) is pretty different

* Shader specialization for new Vulkan required state (fixes remaining alpha test issues, vertex stretching on AMD on Crash Bandicoot, etc)

* Check if the subgroup size is supported before passing a explicit size

* Only enable ShaderFloat64 if the GPU supports it

* We don't need to recompile shaders if alpha test state changed but alpha test is disabled

* Enable shader cache on Vulkan and implement MultiplyHighS32/U32 on SPIR-V (missed those before)

* Fix pipeline state saving before it is updated.

This should fix a few warnings and potential stutters due to bad pipeline states being saved in the cache. You may need to clear your guest cache.

* Allow null samplers on OpenGL backend

* _unit0Sampler should be set only for binding 0

* Remove unused PipelineConverter format variable (was causing IOR)

* Raise textures limit to 64 on Vulkan

* No need to pack the shader binaries if shader cache is disabled

* Fix backbuffer not being cleared and scissor not being re-enabled on OpenGL

* Do not clear unbound framebuffer color attachments

* Geometry shader passthrough emulation

* Consolidate UpdateDepthMode and GetDepthMode implementation

* Fix A1B5G5R5 texture format and support R4G4 on Vulkan

* Add barrier before use of some modified images

* Report 32 bit query result on AMD windows (smo issue)

* Add texture recompression support (disabled for now)

It recompresses ASTC textures into BC7, which might reduce VRAM usage significantly on games that uses ASTC textures

* Do not report R4G4 format as supported on Vulkan

It was causing mario head to become white on Super Mario 64 (???)

* Improvements to -1 to 1 depth mode.

- Transformation is only applied on the last stage in the vertex pipeline.
- Should fix some issues with geometry and tessellation (hopefully)
- Reading back FragCoord Z on fragment will transform back to -1 to 1.

* Geometry Shader index count from ThreadsPerInputPrimitive

Generally fixes SPIR-V emitting too many triangles, may change games in OpenGL

* Remove gl_FragDepth scaling

This is always 0-1; the other two issues were causing the problems. Fixes regression with Xenoblade.

* Add Gl StencilOp enum values to Vulkan

* Update guest cache to v1.1 (due to specialization state changes)

This will explode your shader cache from earlier vulkan build, but it must be done. 😔

* Vulkan/SPIR-V support for viewport inverse

* Fix typo

* Don't create query pools for unsupported query types

* Return of the Vector Indexing Bug

One day, everyone will get this right.

* Check for transform feedback query support

Sometimes transform feedback is supported without the query type.

* Fix gl_FragCoord.z transformation

FragCoord.z is always in 0-1, even when the real depth range is -1 to 1. Turns out the only bug was geo and tess stage outputs.

Fixes Pokemon Sword/Shield, possibly others.

* Fix Avalonia Rebase

Vulkan is currently not available on Avalonia, but the build does work and you can use opengl.

* Fix headless build

* Add support for BC6 and BC7 decompression, decompress all BC formats if they are not supported by the host

* Fix BCn 4/5 conversion, GetTextureTarget

BCn 4/5 could generate invalid data when a line's size in bytes was not divisible by 4, which both backends expect.

GetTextureTarget was not creating a view with the replacement format.

* Fix dependency

* Fix inverse viewport transform vector type on SPIR-V

* Do not require null descriptors support

* If MultiViewport is not supported, do not try to set more than one viewport/scissor

* Bounds check on bitmap add.

* Flush queries on attachment change rather than program change

Occlusion queries are usually used in a depth only pass so the attachments changing is a better indication of the query block ending.

Write mask changes are also considered since some games do depth only pass by setting 0 write mask on all the colour targets.

* Add support for avalonia (#6)

* add avalonia support

* only lock around skia flush

* addressed review

* cleanup

* add fallback size if avalonia attempts to render but the window size is 0. read desktop scale after enabling dpi check

* fix getting window handle on linux. skip render is size is 0

* Combine non-buffer with buffer image descriptor sets

* Support multisample texture copy with automatic resolve on Vulkan

* Remove old CompileShader methods from the Vulkan backend

* Add minimal pipeline layouts that only contains used bindings

They are used by helper shaders, the intention is avoiding needing to recompile the shaders (from GLSL to SPIR-V) if the bindings changes on the translated guest shaders

* Pre-compile helper shader as SPIR-V, and some fixes

* Remove pre-compiled shaderc binary for Windows as its no longer needed by default

* Workaround RADV crash

Enabling the descriptor indexing extension, even if it is not used, forces the radv driver to use "bolist".

* Use RobustBufferAccess on NVIDIA gpus

Avoids the SMO waterfall triangle on older NVIDIA gpus.

* Implement GPU selector and expose texture recompression on the UI and config

* Fix and enable background compute shader compilation

Also disables warnings from shader cache pipeline misses.

* Fix error due to missing subpass dependency when Attachment Write -> Shader Read barriers are added

* If S8D24 is not supported, use D32FS8

* Ensure all fences are destroyed on dispose

* Pre-allocate arrays up front on DescriptorSetUpdater, allows the removal of some checks

* Add missing clear layer parameter after rebase

* Use selected gpu from config for avalonia (#7)

* use configured device

* address review

* Fix D32S8 copy workaround (AMD)

Fixes water in Pokemon Legends Arceus on AMD GPUs. Possibly fixes other things.

* Use push descriptors for uniform buffer updates (disabled for now)

* Push descriptor support check, buffer redundancy checks

Should make push descriptors faster, needs more testing though.

* Increase light command buffer pool to 2 command buffers, throw rather than returning invalid cbs

* Adjust bindings array sizes

* Force submit command buffers if memory in use by its resources is high

* Add workaround for AMD GCN cubemap view sins

`ImageCreateCubeCompatibleBit` seems to generally break 2D array textures with mipmaps... even if they are eventually aliased as a cubemap with mipmaps. Forcing a copy here works around the issue.

This could be used in future if enabling this bit reduces performance on certain GPUs. (mobile class is generally a worry)

Currently also enabled on Linux as I don't know if they managed to dodge this bug (someone please tell me). Not enabled on Vega at the moment, but easy to add if the issue is there.

* Add mobile, non-RX variants to the GCN regex.

Also make sure that the 3 digit ones only include numbers starting with 7 or 8.

* Increase image limit per stage from 8 to 16

Xenoblade Chronicles 2 was hiting the limit of 8

* Minor code cleanup

* Fix NRE caused by SupportBufferUpdater calling pipeline ClearBuffer

* Add gpu selector to Avalonia (#8)

* Add gpu selector to avalonia settings

* show backend label on window

* some fixes

* address review

* Minor changes to the Avalonia UI

* Update graphics window UI and locales. (#9)

* Update xaml and update locales

* locale updates

Did my best here but likely needs to be checked by native speakers, especially the use of ampersands in greek, russian and turkish?

* Fix locales with more (?) correct translations.

* add separator to render widget

* fix spanish and portuguese

* Add new IdList, replaces buffer list that could not remove elements and had unbounded growth

* Don't crash the settings window if Vulkan is not supported

* Fix Actions menu not being clickable on GTK UI after relaunch

* Rename VulkanGraphicsDevice to VulkanRenderer and Renderer to OpenGLRenderer

* Fix IdList and make it not thread safe

* Revert useless OpenGL format table changes

* Fix headless project build

* List throws ArgumentOutOfRangeException

* SPIR-V: Fix tessellation

* Increase shader cache version due to tessellation fix

* Reduce number of Sync objects created (improves perf in some specific titles)

* Fix vulkan validation errors for NPOT compressed upload and GCN workaround.

* Add timestamp to the shader cache and force rebuild if host cache is outdated

* Prefer Mail box present mode for popups (#11)

* Prefer Mail box present mode

* fix debug

* switch present mode when vsync is toggled

* only disable vsync on the main window

* SPIR-V: Fix geometry shader input load with transform feedback

* BC7 Encoder: Prefer more precision on alpha rather than RGB when alpha is 0

* Fix Avalonia build

* Address initial PR feedback

* Only set transform feedback outputs on last vertex stage

* Address riperiperi PR feedback

* Remove outdated comment

* Remove unused constructor

* Only throw for negative results

* Throw for QueueSubmit and other errors

No point in delaying the inevitable

* Transform feedback decorations inside gl_PerVertex struct breaks the NVIDIA compiler

* Fix some resolution scale issues

* No need for two UpdateScale calls

* Fix comments on SPIR-V generator project

* Try to fix shader local memory size

On DOOM, a shader is using local memory, but both Low and High size are 0, CRS size is 1536, it seems to store on that region?

* Remove RectangleF that is now unused

* Fix ImageGather with multiple offsets

Needs ImageGatherExtended capability, and must use `ConstantComposite` instead of `CompositeConstruct`

* Address PR feedback from jD in all projects except Avalonia

* Address most of jD PR feedback on Avalonia

* Remove unsafe

* Fix VulkanSkiaGpu

* move present mode request out of Create Swapchain method

* split more parts of create swapchain

* addressed reviews

* addressed review

* Address second batch of jD PR feedback

* Fix buffer <-> image copy row length and height alignment

AlignUp helper does not support NPOT alignment, and ASTC textures can have NPOT block sizes

* Better fix for NPOT alignment issue

* Use switch expressions on Vulkan EnumConversion

Thanks jD

* Fix Avalonia build

* Add Vulkan selection prompt on startup

* Grammar fixes on Vulkan prompt message

* Add missing Vulkan migration flag

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
Co-authored-by: Emmanuel Hansen <emmausssss@gmail.com>
Co-authored-by: MutantAura <44103205+MutantAura@users.noreply.github.com>
This commit is contained in:
gdkchan 2022-07-31 18:26:06 -03:00 committed by GitHub
parent 14ce9e1567
commit 2232e4ae87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
302 changed files with 38968 additions and 3663 deletions

View file

@ -1,5 +1,6 @@
using ARMeilleure.Translation;
using ARMeilleure.Translation.PTC;
using Avalonia;
using Avalonia.Input;
using Avalonia.Threading;
using LibHac.Tools.FsSystem;
@ -13,6 +14,7 @@ using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
@ -22,6 +24,7 @@ using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
@ -366,6 +369,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
_gpuCancellationTokenSource.Cancel();
_gpuCancellationTokenSource.Dispose();
@ -587,7 +591,23 @@ namespace Ryujinx.Ava
{
VirtualFileSystem.ReloadKeySet();
IRenderer renderer = new Renderer();
IRenderer renderer;
if (Program.UseVulkan)
{
var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
renderer = new VulkanRenderer(vulkan.Instance.InternalHandle,
vulkan.Device.InternalHandle,
vulkan.PhysicalDevice.InternalHandle,
vulkan.Device.Queue.InternalHandle,
vulkan.PhysicalDevice.QueueFamilyIndex,
vulkan.Device.Lock);
}
else
{
renderer = new OpenGLRenderer();
}
IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver();
BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
@ -795,9 +815,12 @@ namespace Ryujinx.Ava
_renderer.ScreenCaptured += Renderer_ScreenCaptured;
(_renderer as Renderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Renderer.GameContext));
if (!Program.UseVulkan)
{
(_renderer as OpenGLRenderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext((Renderer as OpenGLRendererControl).GameContext));
Renderer.MakeCurrent();
}
Device.Gpu.Renderer.Initialize(_glLogLevel);
@ -856,16 +879,15 @@ namespace Ryujinx.Ava
dockedMode += $" ({scale}x)";
}
string vendor = _renderer is Renderer renderer ? renderer.GpuVendor : "";
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync,
Device.GetVolume(),
Program.UseVulkan ? "Vulkan" : "OpenGL",
dockedMode,
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
LocaleManager.Instance["Game"] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
$"GPU: {vendor}"));
$"GPU: {_renderer.GetHardwareInfo().GpuVendor}"));
Renderer.Present(image);
}

View file

@ -122,7 +122,7 @@
"SettingsTabSystemExpandDramSize": "Erweitere DRAM Größe auf 6GB",
"SettingsTabSystemIgnoreMissingServices": "Ignoriere fehlende Dienste",
"SettingsTabGraphics": "Grafik",
"SettingsTabGraphicsEnhancements": "Verbesserungen",
"SettingsTabGraphicsAPI": "Grafik-API",
"SettingsTabGraphicsEnableShaderCache": "Aktiviere den Shader Cache",
"SettingsTabGraphicsAnisotropicFiltering": "Anisotrope Filterung:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Auto",
@ -416,7 +416,7 @@
"CommonFavorite": "Favoriten",
"OrderAscending": "Aufsteigend",
"OrderDescending": "Absteigend",
"SettingsTabGraphicsFeatures": "Erweiterungen",
"SettingsTabGraphicsFeatures": "Erweiterungen & Verbesserungen",
"ErrorWindowTitle": "Fehler-Fenster",
"ToggleDiscordTooltip": "Aktiviert/Deaktiviert Discord Rich Presence",
"AddGameDirBoxTooltip": "Gibt das Spielverzeichnis an, das der Liste hinzuzufügt wird",

View file

@ -122,7 +122,7 @@
"SettingsTabSystemExpandDramSize": "Επέκταση μεγέθους DRAM στα 6GB",
"SettingsTabSystemIgnoreMissingServices": "Αγνόηση υπηρεσιών που λείπουν",
"SettingsTabGraphics": "Γραφικά",
"SettingsTabGraphicsEnhancements": "Βελτιώσεις",
"SettingsTabGraphicsAPI": "API Γραφικά",
"SettingsTabGraphicsEnableShaderCache": "Ενεργοποίηση Προσωρινής Μνήμης Shader",
"SettingsTabGraphicsAnisotropicFiltering": "Ανισότροπο Φιλτράρισμα:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Αυτόματο",
@ -416,7 +416,7 @@
"CommonFavorite": "Αγαπημένα",
"OrderAscending": "Αύξουσα",
"OrderDescending": "Φθίνουσα",
"SettingsTabGraphicsFeatures": "Χαρακτηριστικά",
"SettingsTabGraphicsFeatures": "Χαρακτηριστικά & Βελτιώσεις",
"ErrorWindowTitle": "Παράθυρο σφάλματος",
"ToggleDiscordTooltip": "Ενεργοποιεί ή απενεργοποιεί την Εμπλουτισμένη Παρουσία σας στο Discord",
"AddGameDirBoxTooltip": "Εισαγάγετε μία τοποθεσία παιχνιδιών για προσθήκη στη λίστα",

View file

@ -122,8 +122,8 @@
"SettingsTabSystemExpandDramSize": "Expand DRAM Size to 6GB",
"SettingsTabSystemIgnoreMissingServices": "Ignore Missing Services",
"SettingsTabGraphics": "Graphics",
"SettingsTabGraphicsEnhancements": "Enhancements",
"SettingsTabGraphicsEnableShaderCache": "Shader Cache",
"SettingsTabGraphicsAPI": "Graphics API",
"SettingsTabGraphicsEnableShaderCache": "Enable Shader Cache",
"SettingsTabGraphicsAnisotropicFiltering": "Anisotropic Filtering:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Auto",
"SettingsTabGraphicsAnisotropicFiltering2x": "2x",
@ -416,7 +416,7 @@
"CommonFavorite": "Favorite",
"OrderAscending": "Ascending",
"OrderDescending": "Descending",
"SettingsTabGraphicsFeatures": "Features",
"SettingsTabGraphicsFeatures": "Features & Enhancements",
"ErrorWindowTitle": "Error Window",
"ToggleDiscordTooltip": "Choose whether or not to display Ryujinx on your \"currently playing\" Discord activity",
"AddGameDirBoxTooltip": "Enter a game directory to add to the list",
@ -579,5 +579,14 @@
"SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:",
"SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:",
"UserProfilesName": "Name:",
"UserProfilesUserId" : "User Id:"
"UserProfilesUserId" : "User Id:",
"SettingsTabGraphicsBackend": "Graphics Backend",
"SettingsTabGraphicsBackendTooltip": "Graphics Backend to use",
"SettingsEnableTextureRecompression": "Enable Texture Recompression",
"SettingsEnableTextureRecompressionTooltip": "Compresses certain textures in order to reduce VRAM usage.\n\nRecommended for use with GPUs that have less than 4GB VRAM.\n\nLeave OFF if unsure.",
"SettingsTabGraphicsPreferredGpu": "Preferred GPU",
"SettingsTabGraphicsPreferredGpuTooltip": "Select the graphics card that will be used with the Vulkan graphics backend.\n\nDoes not affect the GPU that OpenGL will use.\n\nSet to the GPU flagged as \"dGPU\" if unsure. If there isn't one, leave untouched.",
"SettingsAppRequiredRestartMessage": "Ryujinx Restart Required",
"SettingsGpuBackendRestartMessage": "Graphics Backend or Gpu settings have been modified. This will require a restart to be applied",
"SettingsGpuBackendRestartSubMessage": "Do you want to restart now?"
}

View file

@ -122,8 +122,8 @@
"SettingsTabSystemExpandDramSize": "Expandir DRAM a 6GB",
"SettingsTabSystemIgnoreMissingServices": "Ignorar servicios no implementados",
"SettingsTabGraphics": "Gráficos",
"SettingsTabGraphicsEnhancements": "Mejoras",
"SettingsTabGraphicsEnableShaderCache": "Caché de sombreadores",
"SettingsTabGraphicsAPI": "API de gráficos",
"SettingsTabGraphicsEnableShaderCache": "Habilitar caché de sombreadores",
"SettingsTabGraphicsAnisotropicFiltering": "Filtro anisotrópico:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Auto",
"SettingsTabGraphicsAnisotropicFiltering2x": "x2",
@ -416,7 +416,7 @@
"CommonFavorite": "Favorito",
"OrderAscending": "Ascendente",
"OrderDescending": "Descendente",
"SettingsTabGraphicsFeatures": "Funcionalidades",
"SettingsTabGraphicsFeatures": "Funcionalidades & Mejoras",
"ErrorWindowTitle": "Ventana de error",
"ToggleDiscordTooltip": "Elige si muestras Ryujinx o no en tu actividad de Discord cuando lo estés usando",
"AddGameDirBoxTooltip": "Elige un directorio de juegos para mostrar en la ventana principal",

View file

@ -115,7 +115,7 @@
"SettingsTabSystemExpandDramSize": "Augmenter la taille de la DRAM à 6GB",
"SettingsTabSystemIgnoreMissingServices": "Ignorer les services manquant",
"SettingsTabGraphics": "Graphique",
"SettingsTabGraphicsEnhancements": "Améliorations",
"SettingsTabGraphicsAPI": "API Graphique",
"SettingsTabGraphicsEnableShaderCache": "Activer le cache des shaders",
"SettingsTabGraphicsAnisotropicFiltering": "Filtrage anisotrope:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Auto",
@ -138,6 +138,7 @@
"SettingsTabGraphicsAspectRatioStretch": "Écran étiré",
"SettingsTabGraphicsDeveloperOptions": "Options développeur",
"SettingsTabGraphicsShaderDumpPath": "Chemin du dossier de dump des shaders:",
"SettingsTabGraphicsFeatures": "Fonctionnalités & Améliorations",
"SettingsTabLogging": "Journaux",
"SettingsTabLoggingLogging": "Journaux",
"SettingsTabLoggingEnableLoggingToFile": "Activer la sauvegarde des journaux vers un fichier",

View file

@ -122,7 +122,7 @@
"SettingsTabSystemExpandDramSize": "Espandi dimensione DRAM a 6GB",
"SettingsTabSystemIgnoreMissingServices": "Ignora servizi mancanti",
"SettingsTabGraphics": "Grafica",
"SettingsTabGraphicsEnhancements": "Miglioramenti",
"SettingsTabGraphicsAPI": "API Grafiche",
"SettingsTabGraphicsEnableShaderCache": "Attiva Shader Cache",
"SettingsTabGraphicsAnisotropicFiltering": "Filtro anisotropico:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Auto",
@ -416,7 +416,7 @@
"CommonFavorite": "Preferito",
"OrderAscending": "Crescente",
"OrderDescending": "Decrescente",
"SettingsTabGraphicsFeatures": "Funzionalità",
"SettingsTabGraphicsFeatures": "Funzionalità & Miglioramenti",
"ErrorWindowTitle": "Finestra errore",
"ToggleDiscordTooltip": "Attiva o disattiva Discord Rich Presence",
"AddGameDirBoxTooltip": "Inserisci la directory di un gioco per aggiungerlo alla lista",

View file

@ -122,7 +122,7 @@
"SettingsTabSystemExpandDramSize": "DRAM 크기를 6GB로 확장",
"SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시",
"SettingsTabGraphics": "제도법",
"SettingsTabGraphicsEnhancements": "개선 사항",
"SettingsTabGraphicsAPI": "그래픽 API",
"SettingsTabGraphicsEnableShaderCache": "셰이더 캐시 활성화",
"SettingsTabGraphicsAnisotropicFiltering": "이방성 필터링 :",
"SettingsTabGraphicsAnisotropicFilteringAuto": "자동적 인",
@ -415,7 +415,7 @@
"CommonFavorite": "가장 좋아하는",
"OrderAscending": "오름차순",
"OrderDescending": "내림차순",
"SettingsTabGraphicsFeatures": "특징",
"SettingsTabGraphicsFeatures": "특징ㆍ개선 사항",
"ErrorWindowTitle": "오류 창",
"ToggleDiscordTooltip": "Discord Rich Presence 활성화 또는 비활성화",
"AddGameDirBoxTooltip": "게임 디렉토리를 입력하여 목록에 추가하세요",

View file

@ -122,7 +122,7 @@
"SettingsTabSystemExpandDramSize": "Expandir memória para 6GB",
"SettingsTabSystemIgnoreMissingServices": "Ignorar serviços não implementados",
"SettingsTabGraphics": "Gráficos",
"SettingsTabGraphicsEnhancements": "Melhorias",
"SettingsTabGraphicsAPI": "API gráfica",
"SettingsTabGraphicsEnableShaderCache": "Habilitar cache de shader",
"SettingsTabGraphicsAnisotropicFiltering": "Filtragem anisotrópica:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Auto",
@ -416,7 +416,7 @@
"CommonFavorite": "Favorito",
"OrderAscending": "Ascendente",
"OrderDescending": "Descendente",
"SettingsTabGraphicsFeatures": "Recursos",
"SettingsTabGraphicsFeatures": "Recursos & Melhorias",
"ErrorWindowTitle": "Janela de erro",
"ToggleDiscordTooltip": "Habilita ou desabilita Discord Rich Presence",
"AddGameDirBoxTooltip": "Escreva um diretório de jogo para adicionar à lista",

View file

@ -122,7 +122,7 @@
"SettingsTabSystemExpandDramSize": "Увеличение размера DRAM до 6GB",
"SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы",
"SettingsTabGraphics": "Графика",
"SettingsTabGraphicsEnhancements": "Улучшения",
"SettingsTabGraphicsAPI": "Графические API",
"SettingsTabGraphicsEnableShaderCache": "Включить кэш шейдеров",
"SettingsTabGraphicsAnisotropicFiltering": "Анизотропная фильтрация:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Автоматически",
@ -377,7 +377,7 @@
"DialogUpdateAddUpdateErrorMessage": "Указанный файл не содержит обновления для выбранного заголовка!",
"DialogSettingsBackendThreadingWarningTitle": "Предупреждение: многопоточность в бэкенде",
"DialogSettingsBackendThreadingWarningMessage": "Ryujinx необходимо перезапустить после изменения этой опции, чтобы она полностью применилась. В зависимости от вашей платформы вам может потребоваться вручную отключить собственную многопоточность вашего драйвера при использовании Ryujinx.",
"SettingsTabGraphicsFeaturesOptions": "Функции",
"SettingsTabGraphicsFeaturesOptions": "Функции & Улучшения",
"SettingsTabGraphicsBackendMultithreading": "Многопоточность графического бэкенда:",
"CommonAuto": "Автоматически",
"CommonOff": "Выключен",

View file

@ -122,7 +122,7 @@
"SettingsTabSystemExpandDramSize": "DRAM boyutunu 6GB'a genişlet",
"SettingsTabSystemIgnoreMissingServices": "Eksik Servisleri Görmezden Gel",
"SettingsTabGraphics": "Grafikler",
"SettingsTabGraphicsEnhancements": "İyileştirmeler",
"SettingsTabGraphicsAPI": "Grafikler API",
"SettingsTabGraphicsEnableShaderCache": "Shader Cache'i Etkinleştir",
"SettingsTabGraphicsAnisotropicFiltering": "Anisotropic Filtering:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Otomatik",
@ -416,7 +416,7 @@
"CommonFavorite": "Favori",
"OrderAscending": "Artan",
"OrderDescending": "Azalan",
"SettingsTabGraphicsFeatures": "Özellikler",
"SettingsTabGraphicsFeatures": "Özellikler & İyileştirmeler",
"ErrorWindowTitle": "Hata Penceresi",
"ToggleDiscordTooltip": "Discord Rich Presence'i Aç/Kapat",
"AddGameDirBoxTooltip": "Listeye eklemek için bir oyun dizini ekleyin",

View file

@ -122,7 +122,7 @@
"SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展到 6GB",
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服务",
"SettingsTabGraphics": "图像",
"SettingsTabGraphicsEnhancements": "增强",
"SettingsTabGraphicsAPI": "的图形 API",
"SettingsTabGraphicsEnableShaderCache": "启用着色器缓存",
"SettingsTabGraphicsAnisotropicFiltering": "各向异性过滤:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "自动",
@ -416,7 +416,7 @@
"CommonFavorite": "收藏",
"OrderAscending": "从小到大",
"OrderDescending": "从大到小",
"SettingsTabGraphicsFeatures": "额外功能",
"SettingsTabGraphicsFeatures": "额外功能和增强",
"ErrorWindowTitle": "错误窗口",
"ToggleDiscordTooltip": "启用或关闭 Discord 详细在线状态展示",
"AddGameDirBoxTooltip": "输入要添加的游戏目录",

View file

@ -3,6 +3,7 @@ using Avalonia;
using Avalonia.OpenGL;
using Avalonia.Rendering;
using Avalonia.Threading;
using Ryujinx.Ava.Ui.Backend;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Common;
@ -11,9 +12,12 @@ using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
using Ryujinx.Common.System;
using Ryujinx.Common.SystemInfo;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.Modules;
using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration;
using Silk.NET.Vulkan.Extensions.EXT;
using Silk.NET.Vulkan.Extensions.KHR;
using System;
using System.Collections.Generic;
using System.IO;
@ -25,17 +29,20 @@ namespace Ryujinx.Ava
internal class Program
{
public static double WindowScaleFactor { get; set; }
public static double ActualScaleFactor { get; set; }
public static string Version { get; private set; }
public static string ConfigurationPath { get; private set; }
public static string CommandLineProfile { get; set; }
public static bool PreviewerDetached { get; private set; }
public static RenderTimer RenderTimer { get; private set; }
public static bool UseVulkan { get; private set; }
[DllImport("user32.dll", SetLastError = true)]
public static extern int MessageBoxA(IntPtr hWnd, string text, string caption, uint type);
private const uint MB_ICONWARNING = 0x30;
private const int BaseDpi = 96;
public static void Main(string[] args)
{
@ -66,7 +73,7 @@ namespace Ryujinx.Ava
EnableMultiTouch = true,
EnableIme = true,
UseEGL = false,
UseGpu = true,
UseGpu = !UseVulkan,
GlProfiles = new List<GlVersion>()
{
new GlVersion(GlProfileType.OpenGL, 4, 3)
@ -75,7 +82,7 @@ namespace Ryujinx.Ava
.With(new Win32PlatformOptions
{
EnableMultitouch = true,
UseWgl = true,
UseWgl = !UseVulkan,
WglProfiles = new List<GlVersion>()
{
new GlVersion(GlProfileType.OpenGL, 4, 3)
@ -84,6 +91,19 @@ namespace Ryujinx.Ava
CompositionBackdropCornerRadius = 8f,
})
.UseSkia()
.With(new Ui.Vulkan.VulkanOptions()
{
ApplicationName = "Ryujinx.Graphics.Vulkan",
VulkanVersion = new Version(1, 2),
MaxQueueCount = 2,
PreferDiscreteGpu = true,
PreferredDevice = !PreviewerDetached ? "" : ConfigurationState.Instance.Graphics.PreferredGpu.Value,
UseDebug = !PreviewerDetached ? false : ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value != GraphicsDebugLevel.None,
})
.With(new SkiaOptions()
{
CustomGpuFactory = UseVulkan ? SkiaGpuFactory.CreateVulkanGpu : null
})
.AfterSetup(_ =>
{
AvaloniaLocator.CurrentMutable
@ -136,9 +156,6 @@ namespace Ryujinx.Ava
}
}
// Make process DPI aware for proper window sizing on high-res screens.
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
// Delete backup files after updating.
Task.Run(Updater.CleanupUpdate);
@ -162,6 +179,18 @@ namespace Ryujinx.Ava
ReloadConfig();
UseVulkan = PreviewerDetached ? ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan : false;
if (UseVulkan)
{
// With a custom gpu backend, avalonia doesn't enable dpi awareness, so the backend must handle it. This isn't so for the opengl backed,
// as that uses avalonia's gpu backend and it's enabled there.
ForceDpiAware.Windows();
}
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
ActualScaleFactor = ForceDpiAware.GetActualScaleFactor() / BaseDpi;
// Logging system information.
PrintSystemInfo();

View file

@ -28,9 +28,12 @@
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
<PackageReference Include="OpenTK.Core" Version="4.7.2" />
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.1-build10" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="OpenTK.Graphics" Version="4.7.2" />
<PackageReference Include="Silk.NET.Vulkan" Version="2.10.1" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.10.1" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.10.1" />
<PackageReference Include="SPB" Version="0.0.4-build17" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
@ -38,6 +41,7 @@
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />

View file

@ -135,7 +135,7 @@ namespace Ryujinx.Ava.Ui.Applet
Dispatcher.UIThread.Post(() =>
{
_hiddenTextBox.Clear();
_parent.GlRenderer.Focus();
_parent.RendererControl.Focus();
_parent = null;
});

View file

@ -0,0 +1,76 @@
using Avalonia;
using System;
using System.Runtime.InteropServices;
using static Ryujinx.Ava.Ui.Backend.Interop;
namespace Ryujinx.Ava.Ui.Backend
{
public abstract class BackendSurface : IDisposable
{
protected IntPtr Display => _display;
private IntPtr _display = IntPtr.Zero;
[DllImport("libX11.so.6")]
public static extern IntPtr XOpenDisplay(IntPtr display);
[DllImport("libX11.so.6")]
public static extern int XCloseDisplay(IntPtr display);
private PixelSize _currentSize;
public IntPtr Handle { get; protected set; }
public bool IsDisposed { get; private set; }
public BackendSurface(IntPtr handle)
{
Handle = handle;
if (OperatingSystem.IsLinux())
{
_display = XOpenDisplay(IntPtr.Zero);
}
}
public PixelSize Size
{
get
{
PixelSize size = new PixelSize();
if (OperatingSystem.IsWindows())
{
GetClientRect(Handle, out var rect);
size = new PixelSize(rect.right, rect.bottom);
}
else if (OperatingSystem.IsLinux())
{
XWindowAttributes attributes = new XWindowAttributes();
XGetWindowAttributes(Display, Handle, ref attributes);
size = new PixelSize(attributes.width, attributes.height);
}
_currentSize = size;
return size;
}
}
public PixelSize CurrentSize => _currentSize;
public virtual void Dispose()
{
if (IsDisposed)
{
throw new ObjectDisposedException(nameof(BackendSurface));
}
IsDisposed = true;
if (_display != IntPtr.Zero)
{
XCloseDisplay(_display);
}
}
}
}

View file

@ -0,0 +1,49 @@
using FluentAvalonia.Interop;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.Ui.Backend
{
public static class Interop
{
[StructLayout(LayoutKind.Sequential)]
public struct XWindowAttributes
{
public int x;
public int y;
public int width;
public int height;
public int border_width;
public int depth;
public IntPtr visual;
public IntPtr root;
public int c_class;
public int bit_gravity;
public int win_gravity;
public int backing_store;
public IntPtr backing_planes;
public IntPtr backing_pixel;
public int save_under;
public IntPtr colormap;
public int map_installed;
public int map_state;
public IntPtr all_event_masks;
public IntPtr your_event_mask;
public IntPtr do_not_propagate_mask;
public int override_direct;
public IntPtr screen;
}
[DllImport("user32.dll")]
public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);
[DllImport("libX11.so.6")]
public static extern int XCloseDisplay(IntPtr display);
[DllImport("libX11.so.6")]
public static extern int XGetWindowAttributes(IntPtr display, IntPtr window, ref XWindowAttributes attributes);
[DllImport("libX11.so.6")]
public static extern IntPtr XOpenDisplay(IntPtr display);
}
}

View file

@ -0,0 +1,26 @@
using Avalonia;
using Avalonia.Skia;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Backend.Vulkan;
namespace Ryujinx.Ava.Ui.Backend
{
public static class SkiaGpuFactory
{
public static ISkiaGpu CreateVulkanGpu()
{
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>() ?? new SkiaOptions();
var platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
if (platformInterface == null)
{
VulkanPlatformInterface.TryInitialize();
}
var gpu = new VulkanSkiaGpu(skiaOptions.MaxGpuResourceSizeBytes);
AvaloniaLocator.CurrentMutable.Bind<VulkanSkiaGpu>().ToConstant(gpu);
return gpu;
}
}
}

View file

@ -0,0 +1,16 @@
using System;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
public static class ResultExtensions
{
public static void ThrowOnError(this Result result)
{
if (result != Result.Success)
{
throw new Exception($"Unexpected API error \"{result}\".");
}
}
}
}

View file

@ -0,0 +1,135 @@
using System;
using Avalonia.Skia;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using SkiaSharp;
namespace Ryujinx.Ava.Ui.Backend.Vulkan
{
internal class VulkanRenderTarget : ISkiaGpuRenderTarget
{
public GRContext GrContext { get; set; }
private readonly VulkanSurfaceRenderTarget _surface;
private readonly IVulkanPlatformSurface _vulkanPlatformSurface;
public VulkanRenderTarget(VulkanPlatformInterface vulkanPlatformInterface, IVulkanPlatformSurface vulkanPlatformSurface)
{
_surface = vulkanPlatformInterface.CreateRenderTarget(vulkanPlatformSurface);
_vulkanPlatformSurface = vulkanPlatformSurface;
}
public void Dispose()
{
_surface.Dispose();
}
public ISkiaGpuRenderSession BeginRenderingSession()
{
var session = _surface.BeginDraw(_vulkanPlatformSurface.Scaling);
bool success = false;
try
{
var disp = session.Display;
var api = session.Api;
var size = session.Size;
var scaling = session.Scaling;
if (size.Width <= 0 || size.Height <= 0 || scaling < 0)
{
size = new Avalonia.PixelSize(1, 1);
scaling = 1;
}
lock (GrContext)
{
GrContext.ResetContext();
var imageInfo = new GRVkImageInfo()
{
CurrentQueueFamily = disp.QueueFamilyIndex,
Format = _surface.ImageFormat,
Image = _surface.Image.Handle,
ImageLayout = (uint)_surface.Image.CurrentLayout,
ImageTiling = (uint)_surface.Image.Tiling,
ImageUsageFlags = _surface.UsageFlags,
LevelCount = _surface.MipLevels,
SampleCount = 1,
Protected = false,
Alloc = new GRVkAlloc()
{
Memory = _surface.Image.MemoryHandle,
Flags = 0,
Offset = 0,
Size = _surface.MemorySize
}
};
var renderTarget =
new GRBackendRenderTarget((int)size.Width, (int)size.Height, 1,
imageInfo);
var surface = SKSurface.Create(GrContext, renderTarget,
GRSurfaceOrigin.TopLeft,
_surface.IsRgba ? SKColorType.Rgba8888 : SKColorType.Bgra8888, SKColorSpace.CreateSrgb());
if (surface == null)
{
throw new InvalidOperationException(
"Surface can't be created with the provided render target");
}
success = true;
return new VulkanGpuSession(GrContext, renderTarget, surface, session);
}
}
finally
{
if (!success)
{
session.Dispose();
}
}
}
public bool IsCorrupted { get; }
internal class VulkanGpuSession : ISkiaGpuRenderSession
{
private readonly GRBackendRenderTarget _backendRenderTarget;
private readonly VulkanSurfaceRenderingSession _vulkanSession;
public VulkanGpuSession(GRContext grContext,
GRBackendRenderTarget backendRenderTarget,
SKSurface surface,
VulkanSurfaceRenderingSession vulkanSession)
{
GrContext = grContext;
_backendRenderTarget = backendRenderTarget;
SkSurface = surface;
_vulkanSession = vulkanSession;
SurfaceOrigin = GRSurfaceOrigin.TopLeft;
}
public void Dispose()
{
lock (_vulkanSession.Display.Lock)
{
SkSurface.Canvas.Flush();
SkSurface.Dispose();
_backendRenderTarget.Dispose();
GrContext.Flush();
_vulkanSession.Dispose();
}
}
public GRContext GrContext { get; }
public SKSurface SkSurface { get; }
public double ScaleFactor => _vulkanSession.Scaling;
public GRSurfaceOrigin SurfaceOrigin { get; }
}
}
}

View file

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Skia;
using Avalonia.X11;
using Ryujinx.Ava.Ui.Vulkan;
using Silk.NET.Vulkan;
using SkiaSharp;
namespace Ryujinx.Ava.Ui.Backend.Vulkan
{
public class VulkanSkiaGpu : ISkiaGpu
{
private readonly VulkanPlatformInterface _vulkan;
private readonly long? _maxResourceBytes;
private GRVkBackendContext _grVkBackend;
private bool _initialized;
public GRContext GrContext { get; private set; }
public VulkanSkiaGpu(long? maxResourceBytes)
{
_vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
_maxResourceBytes = maxResourceBytes;
}
private void Initialize()
{
if (_initialized)
{
return;
}
_initialized = true;
GRVkGetProcedureAddressDelegate getProc = (string name, IntPtr instanceHandle, IntPtr deviceHandle) =>
{
IntPtr addr = IntPtr.Zero;
if (deviceHandle != IntPtr.Zero)
{
addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(deviceHandle), name);
if (addr != IntPtr.Zero)
{
return addr;
}
addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(_vulkan.Device.Handle), name);
if (addr != IntPtr.Zero)
{
return addr;
}
}
addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(_vulkan.Instance.Handle), name);
if (addr == IntPtr.Zero)
{
addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(instanceHandle), name);
}
return addr;
};
_grVkBackend = new GRVkBackendContext()
{
VkInstance = _vulkan.Device.Handle,
VkPhysicalDevice = _vulkan.PhysicalDevice.Handle,
VkDevice = _vulkan.Device.Handle,
VkQueue = _vulkan.Device.Queue.Handle,
GraphicsQueueIndex = _vulkan.PhysicalDevice.QueueFamilyIndex,
GetProcedureAddress = getProc
};
GrContext = GRContext.CreateVulkan(_grVkBackend);
if (_maxResourceBytes.HasValue)
{
GrContext.SetResourceCacheLimit(_maxResourceBytes.Value);
}
}
public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces)
{
foreach (var surface in surfaces)
{
VulkanWindowSurface window;
if (surface is IPlatformHandle handle)
{
window = new VulkanWindowSurface(handle.Handle);
}
else if (surface is X11FramebufferSurface x11FramebufferSurface)
{
// As of Avalonia 0.10.13, an IPlatformHandle isn't passed for linux, so use reflection to otherwise get the window id
var xId = (IntPtr)x11FramebufferSurface.GetType().GetField(
"_xid",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(x11FramebufferSurface);
window = new VulkanWindowSurface(xId);
}
else
{
continue;
}
VulkanRenderTarget vulkanRenderTarget = new VulkanRenderTarget(_vulkan, window);
Initialize();
vulkanRenderTarget.GrContext = GrContext;
return vulkanRenderTarget;
}
return null;
}
public ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session)
{
return null;
}
}
}

View file

@ -0,0 +1,53 @@
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using System;
namespace Ryujinx.Ava.Ui.Backend.Vulkan
{
internal class VulkanWindowSurface : BackendSurface, IVulkanPlatformSurface
{
public float Scaling => (float)Program.ActualScaleFactor;
public PixelSize SurfaceSize => Size;
public VulkanWindowSurface(IntPtr handle) : base(handle)
{
}
public unsafe SurfaceKHR CreateSurface(VulkanInstance instance)
{
if (OperatingSystem.IsWindows())
{
if (instance.Api.TryGetInstanceExtension(new Instance(instance.Handle), out KhrWin32Surface surfaceExtension))
{
var createInfo = new Win32SurfaceCreateInfoKHR() { Hinstance = 0, Hwnd = Handle, SType = StructureType.Win32SurfaceCreateInfoKhr };
surfaceExtension.CreateWin32Surface(new Instance(instance.Handle), createInfo, null, out var surface).ThrowOnError();
return surface;
}
}
else if (OperatingSystem.IsLinux())
{
if (instance.Api.TryGetInstanceExtension(new Instance(instance.Handle), out KhrXlibSurface surfaceExtension))
{
var createInfo = new XlibSurfaceCreateInfoKHR()
{
SType = StructureType.XlibSurfaceCreateInfoKhr,
Dpy = (nint*)Display,
Window = Handle
};
surfaceExtension.CreateXlibSurface(new Instance(instance.Handle), createInfo, null, out var surface).ThrowOnError();
return surface;
}
}
throw new PlatformNotSupportedException("The current platform does not support surface creation.");
}
}
}

View file

@ -0,0 +1,13 @@
using System;
using Avalonia;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
{
public interface IVulkanPlatformSurface : IDisposable
{
float Scaling { get; }
PixelSize SurfaceSize { get; }
SurfaceKHR CreateSurface(VulkanInstance instance);
}
}

View file

@ -0,0 +1,92 @@
using System;
using Avalonia;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan.Surfaces
{
internal class VulkanSurfaceRenderTarget : IDisposable
{
private readonly VulkanPlatformInterface _platformInterface;
private readonly Format _format;
public VulkanImage Image { get; private set; }
public bool IsCorrupted { get; private set; } = true;
public uint MipLevels => Image.MipLevels;
public VulkanSurfaceRenderTarget(VulkanPlatformInterface platformInterface, VulkanSurface surface)
{
_platformInterface = platformInterface;
Display = VulkanDisplay.CreateDisplay(platformInterface.Instance, platformInterface.Device,
platformInterface.PhysicalDevice, surface);
Surface = surface;
// Skia seems to only create surfaces from images with unorm format
IsRgba = Display.SurfaceFormat.Format >= Format.R8G8B8A8Unorm &&
Display.SurfaceFormat.Format <= Format.R8G8B8A8Srgb;
_format = IsRgba ? Format.R8G8B8A8Unorm : Format.B8G8R8A8Unorm;
}
public bool IsRgba { get; }
public uint ImageFormat => (uint) _format;
public ulong MemorySize => Image.MemorySize;
public VulkanDisplay Display { get; }
public VulkanSurface Surface { get; }
public uint UsageFlags => Image.UsageFlags;
public PixelSize Size { get; private set; }
public void Dispose()
{
_platformInterface.Device.WaitIdle();
DestroyImage();
Display?.Dispose();
Surface?.Dispose();
}
public VulkanSurfaceRenderingSession BeginDraw(float scaling)
{
var session = new VulkanSurfaceRenderingSession(Display, _platformInterface.Device, this, scaling);
if (IsCorrupted)
{
IsCorrupted = false;
DestroyImage();
CreateImage();
}
else
{
Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
}
return session;
}
public void Invalidate()
{
IsCorrupted = true;
}
private void CreateImage()
{
Size = Display.Size;
Image = new VulkanImage(_platformInterface.Device, _platformInterface.PhysicalDevice, _platformInterface.Device.CommandBufferPool, ImageFormat, Size);
}
private void DestroyImage()
{
_platformInterface.Device.WaitIdle();
Image?.Dispose();
}
}
}

View file

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanCommandBufferPool : IDisposable
{
private readonly VulkanDevice _device;
private readonly CommandPool _commandPool;
private readonly List<VulkanCommandBuffer> _usedCommandBuffers = new();
public unsafe VulkanCommandBufferPool(VulkanDevice device, VulkanPhysicalDevice physicalDevice)
{
_device = device;
var commandPoolCreateInfo = new CommandPoolCreateInfo
{
SType = StructureType.CommandPoolCreateInfo,
Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit,
QueueFamilyIndex = physicalDevice.QueueFamilyIndex
};
device.Api.CreateCommandPool(_device.InternalHandle, commandPoolCreateInfo, null, out _commandPool)
.ThrowOnError();
}
private CommandBuffer AllocateCommandBuffer()
{
var commandBufferAllocateInfo = new CommandBufferAllocateInfo
{
SType = StructureType.CommandBufferAllocateInfo,
CommandPool = _commandPool,
CommandBufferCount = 1,
Level = CommandBufferLevel.Primary
};
_device.Api.AllocateCommandBuffers(_device.InternalHandle, commandBufferAllocateInfo, out var commandBuffer);
return commandBuffer;
}
public VulkanCommandBuffer CreateCommandBuffer()
{
return new(_device, this);
}
public void FreeUsedCommandBuffers()
{
lock (_usedCommandBuffers)
{
foreach (var usedCommandBuffer in _usedCommandBuffers)
{
usedCommandBuffer.Dispose();
}
_usedCommandBuffers.Clear();
}
}
private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer)
{
lock (_usedCommandBuffers)
{
_usedCommandBuffers.Add(commandBuffer);
}
}
public void Dispose()
{
FreeUsedCommandBuffers();
_device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.Empty);
}
public class VulkanCommandBuffer : IDisposable
{
private readonly VulkanCommandBufferPool _commandBufferPool;
private readonly VulkanDevice _device;
private readonly Fence _fence;
private bool _hasEnded;
private bool _hasStarted;
public IntPtr Handle => InternalHandle.Handle;
internal CommandBuffer InternalHandle { get; }
internal unsafe VulkanCommandBuffer(VulkanDevice device, VulkanCommandBufferPool commandBufferPool)
{
_device = device;
_commandBufferPool = commandBufferPool;
InternalHandle = _commandBufferPool.AllocateCommandBuffer();
var fenceCreateInfo = new FenceCreateInfo()
{
SType = StructureType.FenceCreateInfo,
Flags = FenceCreateFlags.FenceCreateSignaledBit
};
device.Api.CreateFence(device.InternalHandle, fenceCreateInfo, null, out _fence);
}
public void BeginRecording()
{
if (!_hasStarted)
{
_hasStarted = true;
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit
};
_device.Api.BeginCommandBuffer(InternalHandle, beginInfo);
}
}
public void EndRecording()
{
if (_hasStarted && !_hasEnded)
{
_hasEnded = true;
_device.Api.EndCommandBuffer(InternalHandle);
}
}
public void Submit()
{
Submit(null, null, null, _fence);
}
public unsafe void Submit(
ReadOnlySpan<Semaphore> waitSemaphores,
ReadOnlySpan<PipelineStageFlags> waitDstStageMask,
ReadOnlySpan<Semaphore> signalSemaphores,
Fence? fence = null)
{
EndRecording();
if (!fence.HasValue)
{
fence = _fence;
}
fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores)
{
fixed (PipelineStageFlags* pWaitDstStageMask = waitDstStageMask)
{
var commandBuffer = InternalHandle;
var submitInfo = new SubmitInfo
{
SType = StructureType.SubmitInfo,
WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0,
PWaitSemaphores = pWaitSemaphores,
PWaitDstStageMask = pWaitDstStageMask,
CommandBufferCount = 1,
PCommandBuffers = &commandBuffer,
SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0,
PSignalSemaphores = pSignalSemaphores,
};
_device.Api.ResetFences(_device.InternalHandle, 1, fence.Value);
_device.Submit(submitInfo, fence.Value);
}
}
_commandBufferPool.DisposeCommandBuffer(this);
}
public void Dispose()
{
_device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue);
_device.Api.FreeCommandBuffers(_device.InternalHandle, _commandBufferPool._commandPool, 1, InternalHandle);
_device.Api.DestroyFence(_device.InternalHandle, _fence, Span<AllocationCallbacks>.Empty);
}
}
}
}

View file

@ -0,0 +1,67 @@
using System;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanDevice : IDisposable
{
private static object _lock = new object();
public VulkanDevice(Device apiHandle, VulkanPhysicalDevice physicalDevice, Vk api)
{
InternalHandle = apiHandle;
Api = api;
api.GetDeviceQueue(apiHandle, physicalDevice.QueueFamilyIndex, 0, out var queue);
var vulkanQueue = new VulkanQueue(this, queue);
Queue = vulkanQueue;
PresentQueue = vulkanQueue;
CommandBufferPool = new VulkanCommandBufferPool(this, physicalDevice);
}
public IntPtr Handle => InternalHandle.Handle;
internal Device InternalHandle { get; }
public Vk Api { get; }
public VulkanQueue Queue { get; private set; }
public VulkanQueue PresentQueue { get; }
public VulkanCommandBufferPool CommandBufferPool { get; }
public void Dispose()
{
WaitIdle();
CommandBufferPool?.Dispose();
Queue = null;
}
internal void Submit(SubmitInfo submitInfo, Fence fence = default)
{
lock (_lock)
{
Api.QueueSubmit(Queue.InternalHandle, 1, submitInfo, fence).ThrowOnError();
}
}
public void WaitIdle()
{
lock (_lock)
{
Api.DeviceWaitIdle(InternalHandle);
}
}
public void QueueWaitIdle()
{
lock (_lock)
{
Api.QueueWaitIdle(Queue.InternalHandle);
}
}
public object Lock => _lock;
}
}

View file

@ -0,0 +1,439 @@
using System;
using System.Linq;
using System.Threading;
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Ryujinx.Ui.Common.Configuration;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanDisplay : IDisposable
{
private static KhrSwapchain _swapchainExtension;
private readonly VulkanInstance _instance;
private readonly VulkanPhysicalDevice _physicalDevice;
private readonly VulkanSemaphorePair _semaphorePair;
private uint _nextImage;
private readonly VulkanSurface _surface;
private SurfaceFormatKHR _surfaceFormat;
private SwapchainKHR _swapchain;
private Extent2D _swapchainExtent;
private Image[] _swapchainImages;
private VulkanDevice _device { get; }
private ImageView[] _swapchainImageViews = new ImageView[0];
private bool _vsyncStateChanged;
private bool _vsyncEnabled;
public VulkanCommandBufferPool CommandBufferPool { get; set; }
public object Lock => _device.Lock;
private VulkanDisplay(VulkanInstance instance, VulkanDevice device,
VulkanPhysicalDevice physicalDevice, VulkanSurface surface, SwapchainKHR swapchain,
Extent2D swapchainExtent)
{
_instance = instance;
_device = device;
_physicalDevice = physicalDevice;
_swapchain = swapchain;
_swapchainExtent = swapchainExtent;
_surface = surface;
CreateSwapchainImages();
_semaphorePair = new VulkanSemaphorePair(_device);
CommandBufferPool = new VulkanCommandBufferPool(device, physicalDevice);
}
public PixelSize Size { get; private set; }
public uint QueueFamilyIndex => _physicalDevice.QueueFamilyIndex;
internal SurfaceFormatKHR SurfaceFormat
{
get
{
if (_surfaceFormat.Format == Format.Undefined)
{
_surfaceFormat = _surface.GetSurfaceFormat(_physicalDevice);
}
return _surfaceFormat;
}
}
public void Dispose()
{
_device.WaitIdle();
_semaphorePair?.Dispose();
DestroyCurrentImageViews();
_swapchainExtension.DestroySwapchain(_device.InternalHandle, _swapchain, Span<AllocationCallbacks>.Empty);
CommandBufferPool.Dispose();
}
private static unsafe SwapchainKHR CreateSwapchain(VulkanInstance instance, VulkanDevice device,
VulkanPhysicalDevice physicalDevice, VulkanSurface surface, out Extent2D swapchainExtent,
SwapchainKHR? oldswapchain = null, bool vsyncEnabled = true)
{
if (_swapchainExtension == null)
{
instance.Api.TryGetDeviceExtension(instance.InternalHandle, device.InternalHandle, out _swapchainExtension);
}
while (!surface.CanSurfacePresent(physicalDevice))
{
Thread.Sleep(16);
}
VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfaceCapabilities(physicalDevice.InternalHandle,
surface.ApiHandle, out var capabilities);
var imageCount = capabilities.MinImageCount + 1;
if (capabilities.MaxImageCount > 0 && imageCount > capabilities.MaxImageCount)
{
imageCount = capabilities.MaxImageCount;
}
var surfaceFormat = surface.GetSurfaceFormat(physicalDevice);
bool supportsIdentityTransform = capabilities.SupportedTransforms.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformIdentityBitKhr);
bool isRotated = capabilities.CurrentTransform.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformRotate90BitKhr) ||
capabilities.CurrentTransform.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformRotate270BitKhr);
swapchainExtent = GetSwapchainExtent(surface, capabilities);
CompositeAlphaFlagsKHR compositeAlphaFlags = GetSuitableCompositeAlphaFlags(capabilities);
PresentModeKHR presentMode = GetSuitablePresentMode(physicalDevice, surface, vsyncEnabled);
var swapchainCreateInfo = new SwapchainCreateInfoKHR
{
SType = StructureType.SwapchainCreateInfoKhr,
Surface = surface.ApiHandle,
MinImageCount = imageCount,
ImageFormat = surfaceFormat.Format,
ImageColorSpace = surfaceFormat.ColorSpace,
ImageExtent = swapchainExtent,
ImageUsage =
ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit,
ImageSharingMode = SharingMode.Exclusive,
ImageArrayLayers = 1,
PreTransform = supportsIdentityTransform && isRotated ?
SurfaceTransformFlagsKHR.SurfaceTransformIdentityBitKhr :
capabilities.CurrentTransform,
CompositeAlpha = compositeAlphaFlags,
PresentMode = presentMode,
Clipped = true,
OldSwapchain = oldswapchain ?? new SwapchainKHR()
};
_swapchainExtension.CreateSwapchain(device.InternalHandle, swapchainCreateInfo, null, out var swapchain)
.ThrowOnError();
if (oldswapchain != null)
{
_swapchainExtension.DestroySwapchain(device.InternalHandle, oldswapchain.Value, null);
}
return swapchain;
}
private static unsafe Extent2D GetSwapchainExtent(VulkanSurface surface, SurfaceCapabilitiesKHR capabilities)
{
Extent2D swapchainExtent;
if (capabilities.CurrentExtent.Width != uint.MaxValue)
{
swapchainExtent = capabilities.CurrentExtent;
}
else
{
var surfaceSize = surface.SurfaceSize;
var width = Math.Clamp((uint)surfaceSize.Width, capabilities.MinImageExtent.Width, capabilities.MaxImageExtent.Width);
var height = Math.Clamp((uint)surfaceSize.Height, capabilities.MinImageExtent.Height, capabilities.MaxImageExtent.Height);
swapchainExtent = new Extent2D(width, height);
}
return swapchainExtent;
}
private static unsafe CompositeAlphaFlagsKHR GetSuitableCompositeAlphaFlags(SurfaceCapabilitiesKHR capabilities)
{
var compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaOpaqueBitKhr;
if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr))
{
compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr;
}
else if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr))
{
compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr;
}
return compositeAlphaFlags;
}
private static unsafe PresentModeKHR GetSuitablePresentMode(VulkanPhysicalDevice physicalDevice, VulkanSurface surface, bool vsyncEnabled)
{
uint presentModesCount;
VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfacePresentModes(physicalDevice.InternalHandle,
surface.ApiHandle,
&presentModesCount, null);
var presentModes = new PresentModeKHR[presentModesCount];
fixed (PresentModeKHR* pPresentModes = presentModes)
{
VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfacePresentModes(physicalDevice.InternalHandle,
surface.ApiHandle, &presentModesCount, pPresentModes);
}
var modes = presentModes.ToList();
var presentMode = PresentModeKHR.PresentModeFifoKhr;
if (!vsyncEnabled && modes.Contains(PresentModeKHR.PresentModeImmediateKhr))
{
presentMode = PresentModeKHR.PresentModeImmediateKhr;
}
else if (modes.Contains(PresentModeKHR.PresentModeMailboxKhr))
{
presentMode = PresentModeKHR.PresentModeMailboxKhr;
}
else if (modes.Contains(PresentModeKHR.PresentModeImmediateKhr))
{
presentMode = PresentModeKHR.PresentModeImmediateKhr;
}
return presentMode;
}
internal static VulkanDisplay CreateDisplay(VulkanInstance instance, VulkanDevice device,
VulkanPhysicalDevice physicalDevice, VulkanSurface surface)
{
var swapchain = CreateSwapchain(instance, device, physicalDevice, surface, out var extent, null, true);
return new VulkanDisplay(instance, device, physicalDevice, surface, swapchain, extent);
}
private unsafe void CreateSwapchainImages()
{
DestroyCurrentImageViews();
Size = new PixelSize((int)_swapchainExtent.Width, (int)_swapchainExtent.Height);
uint imageCount = 0;
_swapchainExtension.GetSwapchainImages(_device.InternalHandle, _swapchain, &imageCount, null);
_swapchainImages = new Image[imageCount];
fixed (Image* pSwapchainImages = _swapchainImages)
{
_swapchainExtension.GetSwapchainImages(_device.InternalHandle, _swapchain, &imageCount, pSwapchainImages);
}
_swapchainImageViews = new ImageView[imageCount];
var surfaceFormat = SurfaceFormat;
for (var i = 0; i < imageCount; i++)
{
_swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format);
}
}
private void DestroyCurrentImageViews()
{
for (var i = 0; i < _swapchainImageViews.Length; i++)
{
_instance.Api.DestroyImageView(_device.InternalHandle, _swapchainImageViews[i], Span<AllocationCallbacks>.Empty);
}
}
internal void ChangeVSyncMode(bool vsyncEnabled)
{
_vsyncStateChanged = true;
_vsyncEnabled = vsyncEnabled;
}
private void Recreate()
{
_device.WaitIdle();
_swapchain = CreateSwapchain(_instance, _device, _physicalDevice, _surface, out _swapchainExtent, _swapchain, _vsyncEnabled);
CreateSwapchainImages();
}
private unsafe ImageView CreateSwapchainImageView(Image swapchainImage, Format format)
{
var componentMapping = new ComponentMapping(
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity);
var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, 1, 0, 1);
var imageCreateInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = swapchainImage,
ViewType = ImageViewType.ImageViewType2D,
Format = format,
Components = componentMapping,
SubresourceRange = subresourceRange
};
_instance.Api.CreateImageView(_device.InternalHandle, imageCreateInfo, null, out var imageView).ThrowOnError();
return imageView;
}
public bool EnsureSwapchainAvailable()
{
if (Size != _surface.SurfaceSize || _vsyncStateChanged)
{
_vsyncStateChanged = false;
Recreate();
return false;
}
return true;
}
internal VulkanCommandBufferPool.VulkanCommandBuffer StartPresentation(VulkanSurfaceRenderTarget renderTarget)
{
_nextImage = 0;
while (true)
{
var acquireResult = _swapchainExtension.AcquireNextImage(
_device.InternalHandle,
_swapchain,
ulong.MaxValue,
_semaphorePair.ImageAvailableSemaphore,
new Fence(),
ref _nextImage);
if (acquireResult == Result.ErrorOutOfDateKhr ||
acquireResult == Result.SuboptimalKhr)
{
Recreate();
}
else
{
acquireResult.ThrowOnError();
break;
}
}
var commandBuffer = CommandBufferPool.CreateCommandBuffer();
commandBuffer.BeginRecording();
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle,
_swapchainImages[_nextImage], ImageLayout.Undefined,
AccessFlags.AccessNoneKhr,
ImageLayout.TransferDstOptimal,
AccessFlags.AccessTransferWriteBit,
1);
return commandBuffer;
}
internal void BlitImageToCurrentImage(VulkanSurfaceRenderTarget renderTarget, CommandBuffer commandBuffer)
{
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer,
renderTarget.Image.InternalHandle.Value, (ImageLayout)renderTarget.Image.CurrentLayout,
AccessFlags.AccessNoneKhr,
ImageLayout.TransferSrcOptimal,
AccessFlags.AccessTransferReadBit,
renderTarget.MipLevels);
var srcBlitRegion = new ImageBlit
{
SrcOffsets = new ImageBlit.SrcOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(renderTarget.Size.Width, renderTarget.Size.Height, 1),
},
DstOffsets = new ImageBlit.DstOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(Size.Width, Size.Height, 1),
},
SrcSubresource = new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ImageAspectColorBit,
BaseArrayLayer = 0,
LayerCount = 1,
MipLevel = 0
},
DstSubresource = new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ImageAspectColorBit,
BaseArrayLayer = 0,
LayerCount = 1,
MipLevel = 0
}
};
_device.Api.CmdBlitImage(commandBuffer, renderTarget.Image.InternalHandle.Value,
ImageLayout.TransferSrcOptimal,
_swapchainImages[_nextImage],
ImageLayout.TransferDstOptimal,
1,
srcBlitRegion,
Filter.Linear);
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer,
renderTarget.Image.InternalHandle.Value, ImageLayout.TransferSrcOptimal,
AccessFlags.AccessTransferReadBit,
(ImageLayout)renderTarget.Image.CurrentLayout,
AccessFlags.AccessNoneKhr,
renderTarget.MipLevels);
}
internal unsafe void EndPresentation(VulkanCommandBufferPool.VulkanCommandBuffer commandBuffer)
{
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle,
_swapchainImages[_nextImage], ImageLayout.TransferDstOptimal,
AccessFlags.AccessNoneKhr,
ImageLayout.PresentSrcKhr,
AccessFlags.AccessNoneKhr,
1);
commandBuffer.Submit(
stackalloc[] { _semaphorePair.ImageAvailableSemaphore },
stackalloc[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit },
stackalloc[] { _semaphorePair.RenderFinishedSemaphore });
var semaphore = _semaphorePair.RenderFinishedSemaphore;
var swapchain = _swapchain;
var nextImage = _nextImage;
Result result;
var presentInfo = new PresentInfoKHR
{
SType = StructureType.PresentInfoKhr,
WaitSemaphoreCount = 1,
PWaitSemaphores = &semaphore,
SwapchainCount = 1,
PSwapchains = &swapchain,
PImageIndices = &nextImage,
PResults = &result
};
lock (_device.Lock)
{
_swapchainExtension.QueuePresent(_device.PresentQueue.InternalHandle, presentInfo);
}
CommandBufferPool.FreeUsedCommandBuffers();
}
}
}

View file

@ -0,0 +1,167 @@
using System;
using Avalonia;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanImage : IDisposable
{
private readonly VulkanDevice _device;
private readonly VulkanPhysicalDevice _physicalDevice;
private readonly VulkanCommandBufferPool _commandBufferPool;
private ImageLayout _currentLayout;
private AccessFlags _currentAccessFlags;
private ImageUsageFlags _imageUsageFlags { get; }
private ImageView? _imageView { get; set; }
private DeviceMemory _imageMemory { get; set; }
internal Image? InternalHandle { get; private set; }
internal Format Format { get; }
internal ImageAspectFlags AspectFlags { get; private set; }
public ulong Handle => InternalHandle?.Handle ?? 0;
public ulong ViewHandle => _imageView?.Handle ?? 0;
public uint UsageFlags => (uint)_imageUsageFlags;
public ulong MemoryHandle => _imageMemory.Handle;
public uint MipLevels { get; private set; }
public PixelSize Size { get; }
public ulong MemorySize { get; private set; }
public uint CurrentLayout => (uint)_currentLayout;
public VulkanImage(
VulkanDevice device,
VulkanPhysicalDevice physicalDevice,
VulkanCommandBufferPool commandBufferPool,
uint format,
PixelSize size,
uint mipLevels = 0)
{
_device = device;
_physicalDevice = physicalDevice;
_commandBufferPool = commandBufferPool;
Format = (Format)format;
Size = size;
MipLevels = mipLevels;
_imageUsageFlags =
ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit |
ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit;
Initialize();
}
public unsafe void Initialize()
{
if (!InternalHandle.HasValue)
{
MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2));
var imageCreateInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
ImageType = ImageType.ImageType2D,
Format = Format,
Extent = new Extent3D((uint?)Size.Width, (uint?)Size.Height, 1),
MipLevels = MipLevels,
ArrayLayers = 1,
Samples = SampleCountFlags.SampleCount1Bit,
Tiling = Tiling,
Usage = _imageUsageFlags,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
Flags = ImageCreateFlags.ImageCreateMutableFormatBit
};
_device.Api.CreateImage(_device.InternalHandle, imageCreateInfo, null, out var image).ThrowOnError();
InternalHandle = image;
_device.Api.GetImageMemoryRequirements(_device.InternalHandle, InternalHandle.Value,
out var memoryRequirements);
var memoryAllocateInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = memoryRequirements.Size,
MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex(
_physicalDevice,
memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit)
};
_device.Api.AllocateMemory(_device.InternalHandle, memoryAllocateInfo, null,
out var imageMemory);
_imageMemory = imageMemory;
_device.Api.BindImageMemory(_device.InternalHandle, InternalHandle.Value, _imageMemory, 0);
MemorySize = memoryRequirements.Size;
var componentMapping = new ComponentMapping(
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity);
AspectFlags = ImageAspectFlags.ImageAspectColorBit;
var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1);
var imageViewCreateInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = InternalHandle.Value,
ViewType = ImageViewType.ImageViewType2D,
Format = Format,
Components = componentMapping,
SubresourceRange = subresourceRange
};
_device.Api
.CreateImageView(_device.InternalHandle, imageViewCreateInfo, null, out var imageView)
.ThrowOnError();
_imageView = imageView;
_currentLayout = ImageLayout.Undefined;
TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
}
}
public ImageTiling Tiling => ImageTiling.Optimal;
internal void TransitionLayout(ImageLayout destinationLayout, AccessFlags destinationAccessFlags)
{
var commandBuffer = _commandBufferPool.CreateCommandBuffer();
commandBuffer.BeginRecording();
VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle, InternalHandle.Value,
_currentLayout,
_currentAccessFlags,
destinationLayout, destinationAccessFlags,
MipLevels);
commandBuffer.EndRecording();
commandBuffer.Submit();
_currentLayout = destinationLayout;
_currentAccessFlags = destinationAccessFlags;
}
public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags)
{
TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags);
}
public unsafe void Dispose()
{
_device.Api.DestroyImageView(_device.InternalHandle, _imageView.Value, null);
_device.Api.DestroyImage(_device.InternalHandle, InternalHandle.Value, null);
_device.Api.FreeMemory(_device.InternalHandle, _imageMemory, null);
_imageView = default;
InternalHandle = default;
_imageMemory = default;
}
}
}

View file

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Silk.NET.Core;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.EXT;
namespace Ryujinx.Ava.Ui.Vulkan
{
public class VulkanInstance : IDisposable
{
private const string EngineName = "Avalonia Vulkan";
private VulkanInstance(Instance apiHandle, Vk api)
{
InternalHandle = apiHandle;
Api = api;
}
public IntPtr Handle => InternalHandle.Handle;
internal Instance InternalHandle { get; }
public Vk Api { get; }
internal static IEnumerable<string> RequiredInstanceExtensions
{
get
{
yield return "VK_KHR_surface";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
yield return "VK_KHR_xlib_surface";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
yield return "VK_KHR_win32_surface";
}
}
}
public void Dispose()
{
Api?.DestroyInstance(InternalHandle, Span<AllocationCallbacks>.Empty);
Api?.Dispose();
}
internal static unsafe VulkanInstance Create(VulkanOptions options)
{
var api = Vk.GetApi();
var applicationName = Marshal.StringToHGlobalAnsi(options.ApplicationName);
var engineName = Marshal.StringToHGlobalAnsi(EngineName);
var enabledExtensions = new List<string>(options.InstanceExtensions);
enabledExtensions.AddRange(RequiredInstanceExtensions);
var applicationInfo = new ApplicationInfo
{
PApplicationName = (byte*)applicationName,
ApiVersion = new Version32((uint)options.VulkanVersion.Major, (uint)options.VulkanVersion.Minor,
(uint)options.VulkanVersion.Build),
PEngineName = (byte*)engineName,
EngineVersion = new Version32(1, 0, 0),
ApplicationVersion = new Version32(1, 0, 0)
};
var enabledLayers = new HashSet<string>();
if (options.UseDebug)
{
enabledExtensions.Add(ExtDebugUtils.ExtensionName);
enabledExtensions.Add(ExtDebugReport.ExtensionName);
if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation"))
enabledLayers.Add("VK_LAYER_KHRONOS_validation");
}
foreach (var layer in options.EnabledLayers)
enabledLayers.Add(layer);
var ppEnabledExtensions = stackalloc IntPtr[enabledExtensions.Count];
var ppEnabledLayers = stackalloc IntPtr[enabledLayers.Count];
for (var i = 0; i < enabledExtensions.Count; i++)
ppEnabledExtensions[i] = Marshal.StringToHGlobalAnsi(enabledExtensions[i]);
var layers = enabledLayers.ToList();
for (var i = 0; i < enabledLayers.Count; i++)
ppEnabledLayers[i] = Marshal.StringToHGlobalAnsi(layers[i]);
var instanceCreateInfo = new InstanceCreateInfo
{
SType = StructureType.InstanceCreateInfo,
PApplicationInfo = &applicationInfo,
PpEnabledExtensionNames = (byte**)ppEnabledExtensions,
PpEnabledLayerNames = (byte**)ppEnabledLayers,
EnabledExtensionCount = (uint)enabledExtensions.Count,
EnabledLayerCount = (uint)enabledLayers.Count
};
api.CreateInstance(in instanceCreateInfo, null, out var instance).ThrowOnError();
Marshal.FreeHGlobal(applicationName);
Marshal.FreeHGlobal(engineName);
for (var i = 0; i < enabledExtensions.Count; i++) Marshal.FreeHGlobal(ppEnabledExtensions[i]);
for (var i = 0; i < enabledLayers.Count; i++) Marshal.FreeHGlobal(ppEnabledLayers[i]);
return new VulkanInstance(instance, api);
}
private static unsafe bool IsLayerAvailable(Vk api, string layerName)
{
uint layerPropertiesCount;
api.EnumerateInstanceLayerProperties(&layerPropertiesCount, null).ThrowOnError();
var layerProperties = new LayerProperties[layerPropertiesCount];
fixed (LayerProperties* pLayerProperties = layerProperties)
{
api.EnumerateInstanceLayerProperties(&layerPropertiesCount, layerProperties).ThrowOnError();
for (var i = 0; i < layerPropertiesCount; i++)
{
var currentLayerName = Marshal.PtrToStringAnsi((IntPtr)pLayerProperties[i].LayerName);
if (currentLayerName == layerName) return true;
}
}
return false;
}
}
}

View file

@ -0,0 +1,59 @@
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal static class VulkanMemoryHelper
{
internal static int FindSuitableMemoryTypeIndex(VulkanPhysicalDevice physicalDevice, uint memoryTypeBits,
MemoryPropertyFlags flags)
{
physicalDevice.Api.GetPhysicalDeviceMemoryProperties(physicalDevice.InternalHandle, out var properties);
for (var i = 0; i < properties.MemoryTypeCount; i++)
{
var type = properties.MemoryTypes[i];
if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i;
}
return -1;
}
internal static unsafe void TransitionLayout(VulkanDevice device,
CommandBuffer commandBuffer,
Image image,
ImageLayout sourceLayout,
AccessFlags sourceAccessMask,
ImageLayout destinationLayout,
AccessFlags destinationAccessMask,
uint mipLevels)
{
var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1);
var barrier = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = sourceAccessMask,
DstAccessMask = destinationAccessMask,
OldLayout = sourceLayout,
NewLayout = destinationLayout,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = image,
SubresourceRange = subresourceRange
};
device.Api.CmdPipelineBarrier(
commandBuffer,
PipelineStageFlags.PipelineStageAllCommandsBit,
PipelineStageFlags.PipelineStageAllCommandsBit,
0,
0,
null,
0,
null,
1,
barrier);
}
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Ui.Vulkan
{
public class VulkanOptions
{
/// <summary>
/// Sets the application name of the Vulkan instance
/// </summary>
public string ApplicationName { get; set; }
/// <summary>
/// Specifies the Vulkan API version to use
/// </summary>
public Version VulkanVersion { get; set; } = new Version(1, 1, 0);
/// <summary>
/// Specifies additional extensions to enable if available on the instance
/// </summary>
public IEnumerable<string> InstanceExtensions { get; set; } = Enumerable.Empty<string>();
/// <summary>
/// Specifies layers to enable if available on the instance
/// </summary>
public IEnumerable<string> EnabledLayers { get; set; } = Enumerable.Empty<string>();
/// <summary>
/// Enables the debug layer
/// </summary>
public bool UseDebug { get; set; }
/// <summary>
/// Selects the first suitable discrete GPU available
/// </summary>
public bool PreferDiscreteGpu { get; set; }
/// <summary>
/// Sets the device to use if available and suitable.
/// </summary>
public string PreferredDevice { get; set; }
/// <summary>
/// Max number of device queues to request
/// </summary>
public uint MaxQueueCount { get; set; }
}
}

View file

@ -0,0 +1,219 @@
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Core;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.Ui.Vulkan
{
public unsafe class VulkanPhysicalDevice
{
private VulkanPhysicalDevice(PhysicalDevice apiHandle, Vk api, uint queueCount, uint queueFamilyIndex)
{
InternalHandle = apiHandle;
Api = api;
QueueCount = queueCount;
QueueFamilyIndex = queueFamilyIndex;
api.GetPhysicalDeviceProperties(apiHandle, out var properties);
DeviceName = Marshal.PtrToStringAnsi((IntPtr)properties.DeviceName);
DeviceId = VulkanInitialization.StringFromIdPair(properties.VendorID, properties.DeviceID);
var version = (Version32)properties.ApiVersion;
ApiVersion = new Version((int)version.Major, (int)version.Minor, 0, (int)version.Patch);
}
internal PhysicalDevice InternalHandle { get; }
internal Vk Api { get; }
public uint QueueCount { get; }
public uint QueueFamilyIndex { get; }
public IntPtr Handle => InternalHandle.Handle;
public string DeviceName { get; }
public string DeviceId { get; }
public Version ApiVersion { get; }
public static Dictionary<PhysicalDevice, PhysicalDeviceProperties> PhysicalDevices { get; private set; }
public static IEnumerable<KeyValuePair<PhysicalDevice, PhysicalDeviceProperties>> SuitableDevices { get; private set; }
internal static void SelectAvailableDevices(VulkanInstance instance,
VulkanSurface surface, bool preferDiscreteGpu, string preferredDevice)
{
uint physicalDeviceCount;
instance.Api.EnumeratePhysicalDevices(instance.InternalHandle, &physicalDeviceCount, null).ThrowOnError();
var physicalDevices = new PhysicalDevice[physicalDeviceCount];
fixed (PhysicalDevice* pPhysicalDevices = physicalDevices)
{
instance.Api.EnumeratePhysicalDevices(instance.InternalHandle, &physicalDeviceCount, pPhysicalDevices)
.ThrowOnError();
}
PhysicalDevices = new Dictionary<PhysicalDevice, PhysicalDeviceProperties>();
foreach (var physicalDevice in physicalDevices)
{
instance.Api.GetPhysicalDeviceProperties(physicalDevice, out var properties);
PhysicalDevices.Add(physicalDevice, properties);
}
SuitableDevices = PhysicalDevices.Where(x => IsSuitableDevice(
instance.Api,
x.Key,
x.Value,
surface.ApiHandle,
out _,
out _));
}
internal static VulkanPhysicalDevice FindSuitablePhysicalDevice(VulkanInstance instance,
VulkanSurface surface, bool preferDiscreteGpu, string preferredDevice)
{
SelectAvailableDevices(instance, surface, preferDiscreteGpu, preferredDevice);
uint queueFamilyIndex = 0;
uint queueCount = 0;
if (!string.IsNullOrWhiteSpace(preferredDevice))
{
var physicalDevice = SuitableDevices.FirstOrDefault(x => VulkanInitialization.StringFromIdPair(x.Value.VendorID, x.Value.DeviceID) == preferredDevice);
queueFamilyIndex = FindSuitableQueueFamily(instance.Api, physicalDevice.Key,
surface.ApiHandle, out queueCount);
if (queueFamilyIndex != int.MaxValue)
{
return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex);
}
}
if (preferDiscreteGpu)
{
var discreteGpus = SuitableDevices.Where(p => p.Value.DeviceType == PhysicalDeviceType.DiscreteGpu);
foreach (var gpu in discreteGpus)
{
queueFamilyIndex = FindSuitableQueueFamily(instance.Api, gpu.Key,
surface.ApiHandle, out queueCount);
if (queueFamilyIndex != int.MaxValue)
{
return new VulkanPhysicalDevice(gpu.Key, instance.Api, queueCount, queueFamilyIndex);
}
}
}
foreach (var physicalDevice in SuitableDevices)
{
queueFamilyIndex = FindSuitableQueueFamily(instance.Api, physicalDevice.Key,
surface.ApiHandle, out queueCount);
if (queueFamilyIndex != int.MaxValue)
{
return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex);
}
}
throw new Exception("No suitable physical device found");
}
private static unsafe bool IsSuitableDevice(Vk api, PhysicalDevice physicalDevice, PhysicalDeviceProperties properties, SurfaceKHR surface,
out uint queueCount, out uint familyIndex)
{
queueCount = 0;
familyIndex = 0;
if (properties.DeviceType == PhysicalDeviceType.Cpu) return false;
var extensionMatches = 0;
uint propertiesCount;
api.EnumerateDeviceExtensionProperties(physicalDevice, (byte*)null, &propertiesCount, null).ThrowOnError();
var extensionProperties = new ExtensionProperties[propertiesCount];
fixed (ExtensionProperties* pExtensionProperties = extensionProperties)
{
api.EnumerateDeviceExtensionProperties(
physicalDevice,
(byte*)null,
&propertiesCount,
pExtensionProperties).ThrowOnError();
for (var i = 0; i < propertiesCount; i++)
{
var extensionName = Marshal.PtrToStringAnsi((IntPtr)pExtensionProperties[i].ExtensionName);
if (VulkanInitialization.RequiredExtensions.Contains(extensionName))
{
extensionMatches++;
}
}
}
if (extensionMatches == VulkanInitialization.RequiredExtensions.Length)
{
familyIndex = FindSuitableQueueFamily(api, physicalDevice, surface, out queueCount);
return familyIndex != uint.MaxValue;
}
return false;
}
internal unsafe string[] GetSupportedExtensions()
{
uint propertiesCount;
Api.EnumerateDeviceExtensionProperties(InternalHandle, (byte*)null, &propertiesCount, null).ThrowOnError();
var extensionProperties = new ExtensionProperties[propertiesCount];
fixed (ExtensionProperties* pExtensionProperties = extensionProperties)
{
Api.EnumerateDeviceExtensionProperties(InternalHandle, (byte*)null, &propertiesCount, pExtensionProperties)
.ThrowOnError();
}
return extensionProperties.Select(x => Marshal.PtrToStringAnsi((IntPtr)x.ExtensionName)).ToArray();
}
private static uint FindSuitableQueueFamily(Vk api, PhysicalDevice physicalDevice, SurfaceKHR surface,
out uint queueCount)
{
const QueueFlags RequiredFlags = QueueFlags.QueueGraphicsBit | QueueFlags.QueueComputeBit;
var khrSurface = new KhrSurface(api.Context);
uint propertiesCount;
api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, &propertiesCount, null);
var properties = new QueueFamilyProperties[propertiesCount];
fixed (QueueFamilyProperties* pProperties = properties)
{
api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, &propertiesCount, pProperties);
}
for (uint index = 0; index < propertiesCount; index++)
{
var queueFlags = properties[index].QueueFlags;
khrSurface.GetPhysicalDeviceSurfaceSupport(physicalDevice, index, surface, out var surfaceSupported)
.ThrowOnError();
if (queueFlags.HasFlag(RequiredFlags) && surfaceSupported)
{
queueCount = properties[index].QueueCount;
return index;
}
}
queueCount = 0;
return uint.MaxValue;
}
}
}

View file

@ -0,0 +1,80 @@
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan;
using System;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanPlatformInterface : IDisposable
{
private static VulkanOptions _options;
private VulkanPlatformInterface(VulkanInstance instance)
{
Instance = instance;
Api = instance.Api;
}
public VulkanPhysicalDevice PhysicalDevice { get; private set; }
public VulkanInstance Instance { get; }
public VulkanDevice Device { get; set; }
public Vk Api { get; private set; }
public VulkanSurfaceRenderTarget MainSurface { get; set; }
public void Dispose()
{
Device?.Dispose();
Instance?.Dispose();
Api?.Dispose();
}
private static VulkanPlatformInterface TryCreate()
{
_options = AvaloniaLocator.Current.GetService<VulkanOptions>() ?? new VulkanOptions();
var instance = VulkanInstance.Create(_options);
return new VulkanPlatformInterface(instance);
}
public static bool TryInitialize()
{
var feature = TryCreate();
if (feature != null)
{
AvaloniaLocator.CurrentMutable.Bind<VulkanPlatformInterface>().ToConstant(feature);
return true;
}
return false;
}
public VulkanSurfaceRenderTarget CreateRenderTarget(IVulkanPlatformSurface platformSurface)
{
var surface = VulkanSurface.CreateSurface(Instance, platformSurface);
if (Device == null)
{
PhysicalDevice = VulkanPhysicalDevice.FindSuitablePhysicalDevice(Instance, surface, _options.PreferDiscreteGpu, _options.PreferredDevice);
var device = VulkanInitialization.CreateDevice(Instance.Api,
PhysicalDevice.InternalHandle,
PhysicalDevice.QueueFamilyIndex,
VulkanInitialization.GetSupportedExtensions(Instance.Api, PhysicalDevice.InternalHandle),
PhysicalDevice.QueueCount);
Device = new VulkanDevice(device, PhysicalDevice, Instance.Api);
}
var renderTarget = new VulkanSurfaceRenderTarget(this, surface);
if (MainSurface == null && surface != null)
{
MainSurface = renderTarget;
MainSurface.Display.ChangeVSyncMode(false);
}
return renderTarget;
}
}
}

View file

@ -0,0 +1,18 @@
using System;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanQueue
{
public VulkanQueue(VulkanDevice device, Queue apiHandle)
{
Device = device;
InternalHandle = apiHandle;
}
public VulkanDevice Device { get; }
public IntPtr Handle => InternalHandle.Handle;
internal Queue InternalHandle { get; }
}
}

View file

@ -0,0 +1,32 @@
using System;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanSemaphorePair : IDisposable
{
private readonly VulkanDevice _device;
public unsafe VulkanSemaphorePair(VulkanDevice device)
{
_device = device;
var semaphoreCreateInfo = new SemaphoreCreateInfo { SType = StructureType.SemaphoreCreateInfo };
_device.Api.CreateSemaphore(_device.InternalHandle, semaphoreCreateInfo, null, out var semaphore).ThrowOnError();
ImageAvailableSemaphore = semaphore;
_device.Api.CreateSemaphore(_device.InternalHandle, semaphoreCreateInfo, null, out semaphore).ThrowOnError();
RenderFinishedSemaphore = semaphore;
}
internal Semaphore ImageAvailableSemaphore { get; }
internal Semaphore RenderFinishedSemaphore { get; }
public unsafe void Dispose()
{
_device.Api.DestroySemaphore(_device.InternalHandle, ImageAvailableSemaphore, null);
_device.Api.DestroySemaphore(_device.InternalHandle, RenderFinishedSemaphore, null);
}
}
}

View file

@ -0,0 +1,75 @@
using System;
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
namespace Ryujinx.Ava.Ui.Vulkan
{
public class VulkanSurface : IDisposable
{
private readonly VulkanInstance _instance;
private readonly IVulkanPlatformSurface _vulkanPlatformSurface;
private VulkanSurface(IVulkanPlatformSurface vulkanPlatformSurface, VulkanInstance instance)
{
_vulkanPlatformSurface = vulkanPlatformSurface;
_instance = instance;
ApiHandle = vulkanPlatformSurface.CreateSurface(instance);
}
internal SurfaceKHR ApiHandle { get; }
internal static KhrSurface SurfaceExtension { get; private set; }
internal PixelSize SurfaceSize => _vulkanPlatformSurface.SurfaceSize;
public void Dispose()
{
SurfaceExtension.DestroySurface(_instance.InternalHandle, ApiHandle, Span<AllocationCallbacks>.Empty);
_vulkanPlatformSurface.Dispose();
}
internal static VulkanSurface CreateSurface(VulkanInstance instance, IVulkanPlatformSurface vulkanPlatformSurface)
{
if (SurfaceExtension == null)
{
instance.Api.TryGetInstanceExtension(instance.InternalHandle, out KhrSurface extension);
SurfaceExtension = extension;
}
return new VulkanSurface(vulkanPlatformSurface, instance);
}
internal bool CanSurfacePresent(VulkanPhysicalDevice physicalDevice)
{
SurfaceExtension.GetPhysicalDeviceSurfaceSupport(physicalDevice.InternalHandle, physicalDevice.QueueFamilyIndex, ApiHandle, out var isSupported);
return isSupported;
}
internal SurfaceFormatKHR GetSurfaceFormat(VulkanPhysicalDevice physicalDevice)
{
Span<uint> surfaceFormatsCount = stackalloc uint[1];
SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, surfaceFormatsCount, Span<SurfaceFormatKHR>.Empty);
Span<SurfaceFormatKHR> surfaceFormats = stackalloc SurfaceFormatKHR[(int)surfaceFormatsCount[0]];
SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, surfaceFormatsCount, surfaceFormats);
if (surfaceFormats.Length == 1 && surfaceFormats[0].Format == Format.Undefined)
{
return new SurfaceFormatKHR(Format.B8G8R8A8Unorm, ColorSpaceKHR.ColorspaceSrgbNonlinearKhr);
}
foreach (var format in surfaceFormats)
{
if (format.Format == Format.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.ColorspaceSrgbNonlinearKhr)
{
return format;
}
}
return surfaceFormats[0];
}
}
}

View file

@ -0,0 +1,48 @@
using System;
using Avalonia;
using Ryujinx.Ava.Ui.Vulkan.Surfaces;
using Silk.NET.Vulkan;
namespace Ryujinx.Ava.Ui.Vulkan
{
internal class VulkanSurfaceRenderingSession : IDisposable
{
private readonly VulkanDevice _device;
private readonly VulkanSurfaceRenderTarget _renderTarget;
private VulkanCommandBufferPool.VulkanCommandBuffer _commandBuffer;
public VulkanSurfaceRenderingSession(VulkanDisplay display, VulkanDevice device,
VulkanSurfaceRenderTarget renderTarget, float scaling)
{
Display = display;
_device = device;
_renderTarget = renderTarget;
Scaling = scaling;
Begin();
}
public VulkanDisplay Display { get; }
public PixelSize Size => _renderTarget.Size;
public Vk Api => _device.Api;
public float Scaling { get; }
private void Begin()
{
if (!Display.EnsureSwapchainAvailable())
{
_renderTarget.Invalidate();
}
}
public void Dispose()
{
_commandBuffer = Display.StartPresentation(_renderTarget);
Display.BlitImageToCurrentImage(_renderTarget, _commandBuffer.InternalHandle);
Display.EndPresentation(_commandBuffer);
}
}
}

View file

@ -0,0 +1,190 @@
using Avalonia;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using SkiaSharp;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
internal class OpenGLRendererControl : RendererControl
{
public int Major { get; }
public int Minor { get; }
public OpenGLContextBase GameContext { get; set; }
public static OpenGLContextBase PrimaryContext =>
AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>()
.PrimaryContext.AsOpenGLContextBase();
private SwappableNativeWindowBase _gameBackgroundWindow;
private IntPtr _fence;
public OpenGLRendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel)
{
Major = major;
Minor = minor;
}
public override void DestroyBackgroundContext()
{
_image = null;
if (_fence != IntPtr.Zero)
{
DrawOperation.Dispose();
GL.DeleteSync(_fence);
}
GlDrawOperation.DeleteFramebuffer();
GameContext?.Dispose();
_gameBackgroundWindow?.Dispose();
}
internal override void Present(object image)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
Image = (int)image;
}).Wait();
if (_fence != IntPtr.Zero)
{
GL.DeleteSync(_fence);
}
_fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
QueueRender();
_gameBackgroundWindow.SwapBuffers();
}
internal override void MakeCurrent()
{
GameContext.MakeCurrent(_gameBackgroundWindow);
}
internal override void MakeCurrent(SwappableNativeWindowBase window)
{
GameContext.MakeCurrent(window);
}
protected override void CreateWindow()
{
var flags = OpenGLContextFlags.Compat;
if (DebugLevel != GraphicsDebugLevel.None)
{
flags |= OpenGLContextFlags.Debug;
}
_gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
_gameBackgroundWindow.Hide();
GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext);
GameContext.Initialize(_gameBackgroundWindow);
}
protected override ICustomDrawOperation CreateDrawOperation()
{
return new GlDrawOperation(this);
}
private class GlDrawOperation : ICustomDrawOperation
{
private static int _framebuffer;
public Rect Bounds { get; }
private readonly OpenGLRendererControl _control;
public GlDrawOperation(OpenGLRendererControl control)
{
_control = control;
Bounds = _control.Bounds;
}
public void Dispose() { }
public static void DeleteFramebuffer()
{
if (_framebuffer == 0)
{
GL.DeleteFramebuffer(_framebuffer);
}
_framebuffer = 0;
}
public bool Equals(ICustomDrawOperation other)
{
return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds;
}
public bool HitTest(Point p)
{
return Bounds.Contains(p);
}
private void CreateRenderTarget()
{
_framebuffer = GL.GenFramebuffer();
}
public void Render(IDrawingContextImpl context)
{
if (_control.Image == null)
{
return;
}
if (_framebuffer == 0)
{
CreateRenderTarget();
}
int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding);
var image = _control.Image;
var fence = _control._fence;
GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, (int)image, 0);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer);
if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
{
return;
}
var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888);
var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat());
GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue);
using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo);
using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
if (surface == null)
{
return;
}
var rect = new Rect(new Point(), _control.RenderSize);
using var snapshot = surface.Snapshot();
skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint());
}
}
}
}

View file

@ -2,65 +2,49 @@
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using SkiaSharp;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
internal class RendererControl : Control
internal abstract class RendererControl : Control
{
private int _image;
protected object _image;
static RendererControl()
{
AffectsRender<RendererControl>(ImageProperty);
}
public readonly static StyledProperty<int> ImageProperty =
AvaloniaProperty.Register<RendererControl, int>(nameof(Image), 0, inherits: true, defaultBindingMode: BindingMode.TwoWay);
public readonly static StyledProperty<object> ImageProperty =
AvaloniaProperty.Register<RendererControl, object>(
nameof(Image),
0,
inherits: true,
defaultBindingMode: BindingMode.TwoWay);
protected int Image
protected object Image
{
get => _image;
set => SetAndRaise(ImageProperty, ref _image, value);
}
public event EventHandler<EventArgs> GlInitialized;
public event EventHandler<EventArgs> RendererInitialized;
public event EventHandler<Size> SizeChanged;
protected Size RenderSize { get; private set; }
public bool IsStarted { get; private set; }
public int Major { get; }
public int Minor { get; }
public GraphicsDebugLevel DebugLevel { get; }
public OpenGLContextBase GameContext { get; set; }
public static OpenGLContextBase PrimaryContext => AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>().PrimaryContext.AsOpenGLContextBase();
private SwappableNativeWindowBase _gameBackgroundWindow;
private bool _isInitialized;
private IntPtr _fence;
protected ICustomDrawOperation DrawOperation { get; private set; }
private GlDrawOperation _glDrawOperation;
public RendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
public RendererControl(GraphicsDebugLevel graphicsDebugLevel)
{
Major = major;
Minor = minor;
DebugLevel = graphicsDebugLevel;
IObservable<Rect> resizeObservable = this.GetObservable(BoundsProperty);
@ -69,7 +53,7 @@ namespace Ryujinx.Ava.Ui.Controls
Focusable = true;
}
private void Resized(Rect rect)
protected void Resized(Rect rect)
{
SizeChanged?.Invoke(this, rect.Size);
@ -77,37 +61,40 @@ namespace Ryujinx.Ava.Ui.Controls
{
RenderSize = rect.Size * VisualRoot.RenderScaling;
_glDrawOperation?.Dispose();
_glDrawOperation = new GlDrawOperation(this);
DrawOperation?.Dispose();
DrawOperation = CreateDrawOperation();
}
}
protected abstract ICustomDrawOperation CreateDrawOperation();
protected abstract void CreateWindow();
public override void Render(DrawingContext context)
{
if (!_isInitialized)
{
CreateWindow();
OnGlInitialized();
OnRendererInitialized();
_isInitialized = true;
}
if (GameContext == null || !IsStarted || Image == 0)
if (!IsStarted || Image == null)
{
return;
}
if (_glDrawOperation != null)
if (DrawOperation != null)
{
context.Custom(_glDrawOperation);
context.Custom(DrawOperation);
}
base.Render(context);
}
protected void OnGlInitialized()
protected void OnRendererInitialized()
{
GlInitialized?.Invoke(this, EventArgs.Empty);
RendererInitialized?.Invoke(this, EventArgs.Empty);
}
public void QueueRender()
@ -115,24 +102,7 @@ namespace Ryujinx.Ava.Ui.Controls
Program.RenderTimer.TickNow();
}
internal void Present(object image)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
Image = (int)image;
}).Wait();
if (_fence != IntPtr.Zero)
{
GL.DeleteSync(_fence);
}
_fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
QueueRender();
_gameBackgroundWindow.SwapBuffers();
}
internal abstract void Present(object image);
internal void Start()
{
@ -145,132 +115,8 @@ namespace Ryujinx.Ava.Ui.Controls
IsStarted = false;
}
public void DestroyBackgroundContext()
{
_image = 0;
if (_fence != IntPtr.Zero)
{
_glDrawOperation.Dispose();
GL.DeleteSync(_fence);
}
GlDrawOperation.DeleteFramebuffer();
GameContext?.Dispose();
_gameBackgroundWindow?.Dispose();
}
internal void MakeCurrent()
{
GameContext.MakeCurrent(_gameBackgroundWindow);
}
internal void MakeCurrent(SwappableNativeWindowBase window)
{
GameContext.MakeCurrent(window);
}
protected void CreateWindow()
{
var flags = OpenGLContextFlags.Compat;
if (DebugLevel != GraphicsDebugLevel.None)
{
flags |= OpenGLContextFlags.Debug;
}
_gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
_gameBackgroundWindow.Hide();
GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext);
GameContext.Initialize(_gameBackgroundWindow);
}
private class GlDrawOperation : ICustomDrawOperation
{
private static int _framebuffer;
public Rect Bounds { get; }
private readonly RendererControl _control;
public GlDrawOperation(RendererControl control)
{
_control = control;
Bounds = _control.Bounds;
}
public void Dispose() { }
public static void DeleteFramebuffer()
{
if (_framebuffer == 0)
{
GL.DeleteFramebuffer(_framebuffer);
}
_framebuffer = 0;
}
public bool Equals(ICustomDrawOperation other)
{
return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds;
}
public bool HitTest(Point p)
{
return Bounds.Contains(p);
}
private void CreateRenderTarget()
{
_framebuffer = GL.GenFramebuffer();
}
public void Render(IDrawingContextImpl context)
{
if (_control.Image == 0)
{
return;
}
if (_framebuffer == 0)
{
CreateRenderTarget();
}
int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding);
var image = _control.Image;
var fence = _control._fence;
GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, image, 0);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer);
if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
{
return;
}
var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888);
var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat());
GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue);
using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo);
using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
if (surface == null)
{
return;
}
var rect = new Rect(new Point(), _control.RenderSize);
using var snapshot = surface.Snapshot();
skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint());
}
}
public abstract void DestroyBackgroundContext();
internal abstract void MakeCurrent();
internal abstract void MakeCurrent(SwappableNativeWindowBase window);
}
}

View file

@ -0,0 +1,153 @@
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using Ryujinx.Ava.Ui.Backend.Vulkan;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Common.Configuration;
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan;
using SkiaSharp;
using SPB.Windowing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Controls
{
internal class VulkanRendererControl : RendererControl
{
private VulkanPlatformInterface _platformInterface;
public VulkanRendererControl(GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel)
{
_platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
}
public override void DestroyBackgroundContext()
{
}
protected override ICustomDrawOperation CreateDrawOperation()
{
return new VulkanDrawOperation(this);
}
protected override void CreateWindow()
{
}
internal override void MakeCurrent()
{
}
internal override void MakeCurrent(SwappableNativeWindowBase window)
{
}
internal override void Present(object image)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
Image = image;
}).Wait();
QueueRender();
}
private class VulkanDrawOperation : ICustomDrawOperation
{
public Rect Bounds { get; }
private readonly VulkanRendererControl _control;
public VulkanDrawOperation(VulkanRendererControl control)
{
_control = control;
Bounds = _control.Bounds;
}
public void Dispose()
{
}
public bool Equals(ICustomDrawOperation other)
{
return other is VulkanDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds;
}
public bool HitTest(Point p)
{
return Bounds.Contains(p);
}
public void Render(IDrawingContextImpl context)
{
if (_control.Image == null || _control.RenderSize.Width == 0 || _control.RenderSize.Height == 0)
{
return;
}
var image = (PresentImageInfo)_control.Image;
if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
{
return;
}
_control._platformInterface.Device.QueueWaitIdle();
var gpu = AvaloniaLocator.Current.GetService<VulkanSkiaGpu>();
var imageInfo = new GRVkImageInfo()
{
CurrentQueueFamily = _control._platformInterface.PhysicalDevice.QueueFamilyIndex,
Format = (uint)Format.R8G8B8A8Unorm,
Image = image.Image.Handle,
ImageLayout = (uint)ImageLayout.ColorAttachmentOptimal,
ImageTiling = (uint)ImageTiling.Optimal,
ImageUsageFlags = (uint)(ImageUsageFlags.ImageUsageColorAttachmentBit
| ImageUsageFlags.ImageUsageTransferSrcBit
| ImageUsageFlags.ImageUsageTransferDstBit),
LevelCount = 1,
SampleCount = 1,
Protected = false,
Alloc = new GRVkAlloc()
{
Memory = image.Memory.Handle,
Flags = 0,
Offset = image.MemoryOffset,
Size = image.MemorySize
}
};
using var backendTexture = new GRBackendRenderTarget(
(int)_control.RenderSize.Width,
(int)_control.RenderSize.Height,
1,
imageInfo);
using var surface = SKSurface.Create(
gpu.GrContext,
backendTexture,
GRSurfaceOrigin.TopLeft,
SKColorType.Rgba8888);
if (surface == null)
{
return;
}
var rect = new Rect(new Point(), _control.RenderSize);
using var snapshot = surface.Snapshot();
skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint());
}
}
}
}

View file

@ -6,16 +6,18 @@ namespace Ryujinx.Ava.Ui.Models
{
public bool VSyncEnabled { get; }
public float Volume { get; }
public string GpuBackend { get; }
public string AspectRatio { get; }
public string DockedMode { get; }
public string FifoStatus { get; }
public string GameStatus { get; }
public string GpuName { get; }
public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName)
public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName)
{
VSyncEnabled = vSyncEnabled;
Volume = volume;
GpuBackend = gpuBackend;
DockedMode = dockedMode;
AspectRatio = aspectRatio;
GameStatus = gameStatus;

View file

@ -197,6 +197,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
private string _pauseKey = "F5";
private string _screenshotkey = "F8";
private float _volume;
private string _backendText;
public ApplicationData SelectedApplication
{
@ -335,7 +336,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
}
public string GpuStatusText
public string GpuNameText
{
get => _gpuStatusText;
set
@ -346,6 +347,17 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
}
public string BackendText
{
get => _backendText;
set
{
_backendText = value;
OnPropertyChanged();
}
}
public string DockedStatusText
{
get => _dockedStatusText;

View file

@ -1,6 +1,8 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
using DynamicData;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
@ -8,18 +10,26 @@ using Ryujinx.Audio.Backends.SoundIo;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Time.TimeZone;
using Ryujinx.Input;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Configuration.System;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone;
namespace Ryujinx.Ava.Ui.ViewModels
@ -101,6 +111,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
public bool IgnoreMissingServices { get; set; }
public bool ExpandDramSize { get; set; }
public bool EnableShaderCache { get; set; }
public bool EnableTextureRecompression { get; set; }
public bool EnableFileLog { get; set; }
public bool EnableStub { get; set; }
public bool EnableInfo { get; set; }
@ -115,6 +126,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
public bool IsSDL2Enabled { get; set; }
public bool EnableCustomTheme { get; set; }
public bool IsCustomResolutionScaleActive => _resolutionScale == 0;
public bool IsVulkanSelected => GraphicsBackendIndex == 0;
public string TimeZone { get; set; }
public string ShaderDumpPath { get; set; }
@ -129,6 +141,18 @@ namespace Ryujinx.Ava.Ui.ViewModels
public int OpenglDebugLevel { get; set; }
public int MemoryMode { get; set; }
public int BaseStyleIndex { get; set; }
public int GraphicsBackendIndex
{
get => graphicsBackendIndex;
set
{
graphicsBackendIndex = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsVulkanSelected));
}
}
public int PreferredGpuIndex { get; set; }
public float Volume
{
@ -148,8 +172,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
public AvaloniaList<TimeZone> TimeZones { get; set; }
public AvaloniaList<string> GameDirectories { get; set; }
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
private KeyboardHotkeys _keyboardHotkeys;
private int graphicsBackendIndex;
private List<string> _gpuIds = new List<string>();
public KeyboardHotkeys KeyboardHotkeys
{
@ -180,12 +207,14 @@ namespace Ryujinx.Ava.Ui.ViewModels
{
GameDirectories = new AvaloniaList<string>();
TimeZones = new AvaloniaList<TimeZone>();
AvailableGpus = new ObservableCollection<ComboBoxItem>();
_validTzRegions = new List<string>();
CheckSoundBackends();
if (Program.PreviewerDetached)
{
LoadAvailableGpus();
LoadCurrentConfiguration();
}
}
@ -197,6 +226,34 @@ namespace Ryujinx.Ava.Ui.ViewModels
IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported;
}
private unsafe void LoadAvailableGpus()
{
_gpuIds = new List<string>();
List<string> names = new List<string>();
if (!Program.UseVulkan)
{
var devices = VulkanRenderer.GetPhysicalDevices();
foreach (var device in devices)
{
_gpuIds.Add(device.Id);
names.Add($"{device.Name} {(device.IsDiscrete ? "(dGpu)" : "")}");
}
}
else
{
foreach (var device in VulkanPhysicalDevice.SuitableDevices)
{
_gpuIds.Add(VulkanInitialization.StringFromIdPair(device.Value.VendorID, device.Value.DeviceID));
var value = device.Value;
var name = value.DeviceName;
names.Add($"{Marshal.PtrToStringAnsi((IntPtr)name)} {(device.Value.DeviceType == PhysicalDeviceType.DiscreteGpu ? "(dGpu)" : "")}");
}
}
AvailableGpus.Clear();
AvailableGpus.AddRange(names.Select(x => new ComboBoxItem() { Content = x }));
}
public void LoadTimeZones()
{
_timeZoneContentManager = new TimeZoneContentManager();
@ -266,6 +323,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
IgnoreMissingServices = config.System.IgnoreMissingServices;
ExpandDramSize = config.System.ExpandRam;
EnableShaderCache = config.Graphics.EnableShaderCache;
EnableTextureRecompression = config.Graphics.EnableTextureRecompression;
EnableFileLog = config.Logger.EnableFileLog;
EnableStub = config.Logger.EnableStub;
EnableInfo = config.Logger.EnableInfo;
@ -286,6 +344,9 @@ namespace Ryujinx.Ava.Ui.ViewModels
ShaderDumpPath = config.Graphics.ShadersDumpPath;
CustomThemePath = config.Ui.CustomThemePath;
BaseStyleIndex = config.Ui.BaseStyle == "Light" ? 0 : 1;
GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value;
PreferredGpuIndex = _gpuIds.Contains(config.Graphics.PreferredGpu) ? _gpuIds.IndexOf(config.Graphics.PreferredGpu) : 0;
Language = (int)config.System.Language.Value;
Region = (int)config.System.Region.Value;
@ -313,7 +374,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
_previousVolumeLevel = Volume;
}
public void SaveSettings()
public async Task SaveSettings()
{
List<string> gameDirs = new List<string>(GameDirectories);
@ -324,6 +385,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
config.System.TimeZone.Value = TimeZone;
}
bool requiresRestart = config.Graphics.GraphicsBackend.Value != (GraphicsBackend)GraphicsBackendIndex;
config.Logger.EnableError.Value = EnableError;
config.Logger.EnableTrace.Value = EnableTrace;
config.Logger.EnableWarn.Value = EnableWarn;
@ -341,6 +404,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
config.HideCursorOnIdle.Value = HideCursorOnIdle;
config.Graphics.EnableVsync.Value = EnableVsync;
config.Graphics.EnableShaderCache.Value = EnableShaderCache;
config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression;
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
config.System.EnablePtc.Value = EnablePptc;
config.System.EnableInternetAccess.Value = EnableInternetAccess;
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
@ -354,6 +419,20 @@ namespace Ryujinx.Ava.Ui.ViewModels
config.System.Language.Value = (Language)Language;
config.System.Region.Value = (Region)Region;
var selectedGpu = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
if (!requiresRestart)
{
var platform = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>();
if (platform != null)
{
var physicalDevice = platform.PhysicalDevice;
requiresRestart = physicalDevice.DeviceId != selectedGpu;
}
}
config.Graphics.PreferredGpu.Value = selectedGpu;
if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex)
{
DriverUtilities.ToggleOGLThreading(GraphicsBackendMultithreadingIndex == (int)BackendThreading.Off);
@ -392,6 +471,20 @@ namespace Ryujinx.Ava.Ui.ViewModels
MainWindow.UpdateGraphicsConfig();
_previousVolumeLevel = Volume;
if (requiresRestart)
{
var choice = await ContentDialogHelper.CreateChoiceDialog(
LocaleManager.Instance["SettingsAppRequiredRestartMessage"],
LocaleManager.Instance["SettingsGpuBackendRestartMessage"],
LocaleManager.Instance["SettingsGpuBackendRestartSubMessage"]);
if (choice)
{
Process.Start(Environment.ProcessPath);
Environment.Exit(0);
}
}
}
public void RevertIfNotSaved()

View file

@ -721,7 +721,21 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding GpuStatusText}"
Text="{Binding BackendText}"
TextAlignment="Left" />
<Border
Width="2"
Height="12"
Margin="2,0"
BorderBrush="Gray"
BorderThickness="1"
IsVisible="{Binding !ShowLoadProgress}" />
<TextBlock
Margin="5,0,5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
Text="{Binding GpuNameText}"
TextAlignment="Left" />
</StackPanel>
<StackPanel

View file

@ -12,6 +12,7 @@ using Ryujinx.Ava.Ui.Applet;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.ViewModels;
using Ryujinx.Ava.Ui.Vulkan;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu;
@ -59,7 +60,7 @@ namespace Ryujinx.Ava.Ui.Windows
internal AppHost AppHost { get; private set; }
public InputManager InputManager { get; private set; }
internal RendererControl GlRenderer { get; private set; }
internal RendererControl RendererControl { get; private set; }
internal MainWindowViewModel ViewModel { get; private set; }
public SettingsWindow SettingsWindow { get; set; }
@ -140,7 +141,8 @@ namespace Ryujinx.Ava.Ui.Windows
ViewModel.AspectRatioStatusText = args.AspectRatio;
ViewModel.GameStatusText = args.GameStatus;
ViewModel.FifoStatusText = args.FifoStatus;
ViewModel.GpuStatusText = args.GpuName;
ViewModel.GpuNameText = args.GpuName;
ViewModel.BackendText = args.GpuBackend;
ViewModel.ShowStatusSeparator = true;
});
@ -237,8 +239,8 @@ namespace Ryujinx.Ava.Ui.Windows
_mainViewContent = MainContent.Content as Control;
GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
RendererControl = Program.UseVulkan ? new VulkanRendererControl(ConfigurationState.Instance.Logger.GraphicsDebugLevel) : new OpenGLRendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
if (!AppHost.LoadGuestApplication().Result)
{
@ -262,7 +264,7 @@ namespace Ryujinx.Ava.Ui.Windows
private void InitializeGame()
{
GlRenderer.GlInitialized += GlRenderer_Created;
RendererControl.RendererInitialized += GlRenderer_Created;
AppHost.StatusUpdatedEvent += Update_StatusBar;
AppHost.AppExit += AppHost_AppExit;
@ -302,14 +304,14 @@ namespace Ryujinx.Ava.Ui.Windows
Dispatcher.UIThread.InvokeAsync(() =>
{
MainContent.Content = GlRenderer;
MainContent.Content = RendererControl;
if (startFullscreen && WindowState != WindowState.FullScreen)
{
ViewModel.ToggleFullscreen();
}
GlRenderer.Focus();
RendererControl.Focus();
});
}
@ -361,8 +363,9 @@ namespace Ryujinx.Ava.Ui.Windows
HandleRelaunch();
});
GlRenderer.GlInitialized -= GlRenderer_Created;
GlRenderer = null;
RendererControl.RendererInitialized -= GlRenderer_Created;
RendererControl = null;
ViewModel.SelectedIcon = null;
@ -513,6 +516,7 @@ namespace Ryujinx.Ava.Ui.Windows
GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy;
GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache;
GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression;
}
public void LoadHotKeys()

View file

@ -504,43 +504,51 @@
VerticalScrollBarVisibility="Auto">
<Border>
<StackPanel
Margin="10, 5"
Margin="10,5"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock FontWeight="Bold" Text="{locale:Locale SettingsTabGraphicsEnhancements}" />
<TextBlock FontWeight="Bold" Text="{locale:Locale SettingsTabGraphicsAPI}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
Text="{locale:Locale SettingsTabGraphicsBackend}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
SelectedIndex="{Binding GraphicsBackendIndex}">
<ComboBoxItem>
<TextBlock Text="Vulkan" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="OpenGL" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
Text="{locale:Locale SettingsTabGraphicsPreferredGpu}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
SelectedIndex="{Binding PreferredGpuIndex}"
Items="{Binding AvailableGpus}"/>
</StackPanel>
<Separator Height="1" />
<TextBlock FontWeight="Bold" Text="{locale:Locale SettingsTabGraphicsFeatures}" />
<StackPanel Orientation="Vertical">
<CheckBox IsChecked="{Binding EnableShaderCache}"
ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}">
<TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" />
</CheckBox>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale AnisotropyTooltip}"
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}"
Width="250" />
<ComboBox SelectedIndex="{Binding MaxAnisotropy}"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale AnisotropyTooltip}">
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" />
</ComboBoxItem>
</ComboBox>
<CheckBox IsChecked="{Binding EnableTextureRecompression}"
ToolTip.Tip="{locale:Locale SettingsEnableTextureRecompressionTooltip}">
<TextBlock Text="{locale:Locale SettingsEnableTextureRecompression}" />
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
@ -580,6 +588,34 @@
Minimum="0.1"
Value="{Binding CustomResolutionScale}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale AnisotropyTooltip}"
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}"
Width="250" />
<ComboBox SelectedIndex="{Binding MaxAnisotropy}"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale AnisotropyTooltip}">
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale AspectRatioTooltip}"
@ -610,8 +646,6 @@
</ComboBox>
</StackPanel>
</StackPanel>
<Separator Height="1" />
<TextBlock FontWeight="Bold" Text="{locale:Locale SettingsTabGraphicsFeatures}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"

View file

@ -1,4 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@ -15,9 +14,9 @@ using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone;
namespace Ryujinx.Ava.Ui.Windows
@ -209,9 +208,9 @@ namespace Ryujinx.Ava.Ui.Windows
}
}
private void SaveButton_Clicked(object sender, RoutedEventArgs e)
private async void SaveButton_Clicked(object sender, RoutedEventArgs e)
{
SaveSettings();
await SaveSettings();
Close();
}
@ -222,14 +221,14 @@ namespace Ryujinx.Ava.Ui.Windows
Close();
}
private void ApplyButton_Clicked(object sender, RoutedEventArgs e)
private async void ApplyButton_Clicked(object sender, RoutedEventArgs e)
{
SaveSettings();
await SaveSettings();
}
private void SaveSettings()
private async Task SaveSettings()
{
ViewModel.SaveSettings();
await ViewModel.SaveSettings();
ControllerSettings?.SaveCurrentProfile();

View file

@ -0,0 +1,8 @@
namespace Ryujinx.Common.Configuration
{
public enum GraphicsBackend
{
Vulkan,
OpenGl
}
}

View file

@ -640,4 +640,15 @@ namespace Ryujinx.Common.Memory
public ref T this[int index] => ref ToSpan()[index];
public Span<T> ToSpan() => MemoryMarshal.CreateSpan(ref _e0, 64);
}
public struct Array73<T> : IArray<T> where T : unmanaged
{
#pragma warning disable CS0169
T _e0;
Array64<T> _other;
Array8<T> _other2;
#pragma warning restore CS0169
public int Length => 73;
public ref T this[int index] => ref ToSpan()[index];
public Span<T> ToSpan() => MemoryMarshal.CreateSpan(ref _e0, 73);
}
}

View file

@ -44,7 +44,7 @@ namespace Ryujinx.Common.System
}
}
public static double GetWindowScaleFactor()
public static double GetActualScaleFactor()
{
double userDpiScale = 96.0;
@ -84,6 +84,13 @@ namespace Ryujinx.Common.System
Logger.Warning?.Print(LogClass.Application, $"Couldn't determine monitor DPI: {e.Message}");
}
return userDpiScale;
}
public static double GetWindowScaleFactor()
{
double userDpiScale = GetActualScaleFactor();
return Math.Min(userDpiScale / _standardDpiScale, _maxScaleFactor);
}
}

View file

@ -11,19 +11,29 @@ namespace Ryujinx.Graphics.GAL
public readonly bool HasVectorIndexingBug;
public readonly bool SupportsAstcCompression;
public readonly bool SupportsBc123Compression;
public readonly bool SupportsBc45Compression;
public readonly bool SupportsBc67Compression;
public readonly bool Supports3DTextureCompression;
public readonly bool SupportsBgraFormat;
public readonly bool SupportsR4G4Format;
public readonly bool SupportsFragmentShaderInterlock;
public readonly bool SupportsFragmentShaderOrderingIntel;
public readonly bool SupportsGeometryShaderPassthrough;
public readonly bool SupportsImageLoadFormatted;
public readonly bool SupportsMismatchingViewFormat;
public readonly bool SupportsCubemapView;
public readonly bool SupportsNonConstantTextureOffset;
public readonly bool SupportsShaderBallot;
public readonly bool SupportsTextureShadowLod;
public readonly bool SupportsViewportSwizzle;
public readonly bool SupportsIndirectParameters;
public readonly uint MaximumUniformBuffersPerStage;
public readonly uint MaximumStorageBuffersPerStage;
public readonly uint MaximumTexturesPerStage;
public readonly uint MaximumImagesPerStage;
public readonly int MaximumComputeSharedMemorySize;
public readonly float MaximumSupportedAnisotropy;
public readonly int StorageBufferOffsetAlignment;
@ -34,18 +44,27 @@ namespace Ryujinx.Graphics.GAL
bool hasFrontFacingBug,
bool hasVectorIndexingBug,
bool supportsAstcCompression,
bool supportsBc123Compression,
bool supportsBc45Compression,
bool supportsBc67Compression,
bool supports3DTextureCompression,
bool supportsBgraFormat,
bool supportsR4G4Format,
bool supportsFragmentShaderInterlock,
bool supportsFragmentShaderOrderingIntel,
bool supportsGeometryShaderPassthrough,
bool supportsImageLoadFormatted,
bool supportsMismatchingViewFormat,
bool supportsCubemapView,
bool supportsNonConstantTextureOffset,
bool supportsShaderBallot,
bool supportsTextureShadowLod,
bool supportsViewportSwizzle,
bool supportsIndirectParameters,
uint maximumUniformBuffersPerStage,
uint maximumStorageBuffersPerStage,
uint maximumTexturesPerStage,
uint maximumImagesPerStage,
int maximumComputeSharedMemorySize,
float maximumSupportedAnisotropy,
int storageBufferOffsetAlignment)
@ -55,18 +74,27 @@ namespace Ryujinx.Graphics.GAL
HasFrontFacingBug = hasFrontFacingBug;
HasVectorIndexingBug = hasVectorIndexingBug;
SupportsAstcCompression = supportsAstcCompression;
SupportsBc123Compression = supportsBc123Compression;
SupportsBc45Compression = supportsBc45Compression;
SupportsBc67Compression = supportsBc67Compression;
Supports3DTextureCompression = supports3DTextureCompression;
SupportsBgraFormat = supportsBgraFormat;
SupportsR4G4Format = supportsR4G4Format;
SupportsFragmentShaderInterlock = supportsFragmentShaderInterlock;
SupportsFragmentShaderOrderingIntel = supportsFragmentShaderOrderingIntel;
SupportsGeometryShaderPassthrough = supportsGeometryShaderPassthrough;
SupportsImageLoadFormatted = supportsImageLoadFormatted;
SupportsMismatchingViewFormat = supportsMismatchingViewFormat;
SupportsCubemapView = supportsCubemapView;
SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset;
SupportsShaderBallot = supportsShaderBallot;
SupportsTextureShadowLod = supportsTextureShadowLod;
SupportsViewportSwizzle = supportsViewportSwizzle;
SupportsIndirectParameters = supportsIndirectParameters;
MaximumUniformBuffersPerStage = maximumUniformBuffersPerStage;
MaximumStorageBuffersPerStage = maximumStorageBuffersPerStage;
MaximumTexturesPerStage = maximumTexturesPerStage;
MaximumImagesPerStage = maximumImagesPerStage;
MaximumComputeSharedMemorySize = maximumComputeSharedMemorySize;
MaximumSupportedAnisotropy = maximumSupportedAnisotropy;
StorageBufferOffsetAlignment = storageBufferOffsetAlignment;

View file

@ -0,0 +1,18 @@
namespace Ryujinx.Graphics.GAL
{
public struct DeviceInfo
{
public readonly string Id;
public readonly string Vendor;
public readonly string Name;
public readonly bool IsDiscrete;
public DeviceInfo(string id, string vendor, string name, bool isDiscrete)
{
Id = id;
Vendor = vendor;
Name = name;
IsDiscrete = isDiscrete;
}
}
}

View file

@ -165,6 +165,120 @@ namespace Ryujinx.Graphics.GAL
public static class FormatExtensions
{
/// <summary>
/// Checks if the texture format is valid to use as image format.
/// </summary>
/// <param name="format">Texture format</param>
/// <returns>True if the texture can be used as image, false otherwise</returns>
public static bool IsImageCompatible(this Format format)
{
switch (format)
{
case Format.R8Unorm:
case Format.R8Snorm:
case Format.R8Uint:
case Format.R8Sint:
case Format.R16Float:
case Format.R16Unorm:
case Format.R16Snorm:
case Format.R16Uint:
case Format.R16Sint:
case Format.R32Float:
case Format.R32Uint:
case Format.R32Sint:
case Format.R8G8Unorm:
case Format.R8G8Snorm:
case Format.R8G8Uint:
case Format.R8G8Sint:
case Format.R16G16Float:
case Format.R16G16Unorm:
case Format.R16G16Snorm:
case Format.R16G16Uint:
case Format.R16G16Sint:
case Format.R32G32Float:
case Format.R32G32Uint:
case Format.R32G32Sint:
case Format.R8G8B8A8Unorm:
case Format.R8G8B8A8Snorm:
case Format.R8G8B8A8Uint:
case Format.R8G8B8A8Sint:
case Format.R16G16B16A16Float:
case Format.R16G16B16A16Unorm:
case Format.R16G16B16A16Snorm:
case Format.R16G16B16A16Uint:
case Format.R16G16B16A16Sint:
case Format.R32G32B32A32Float:
case Format.R32G32B32A32Uint:
case Format.R32G32B32A32Sint:
case Format.R10G10B10A2Unorm:
case Format.R10G10B10A2Uint:
case Format.R11G11B10Float:
return true;
}
return false;
}
/// <summary>
/// Checks if the texture format is valid to use as render target color format.
/// </summary>
/// <param name="format">Texture format</param>
/// <returns>True if the texture can be used as render target, false otherwise</returns>
public static bool IsRtColorCompatible(this Format format)
{
switch (format)
{
case Format.R32G32B32A32Float:
case Format.R32G32B32A32Sint:
case Format.R32G32B32A32Uint:
case Format.R16G16B16A16Unorm:
case Format.R16G16B16A16Snorm:
case Format.R16G16B16A16Sint:
case Format.R16G16B16A16Uint:
case Format.R16G16B16A16Float:
case Format.R32G32Float:
case Format.R32G32Sint:
case Format.R32G32Uint:
case Format.B8G8R8A8Unorm:
case Format.B8G8R8A8Srgb:
case Format.R10G10B10A2Unorm:
case Format.R10G10B10A2Uint:
case Format.R8G8B8A8Unorm:
case Format.R8G8B8A8Srgb:
case Format.R8G8B8A8Snorm:
case Format.R8G8B8A8Sint:
case Format.R8G8B8A8Uint:
case Format.R16G16Unorm:
case Format.R16G16Snorm:
case Format.R16G16Sint:
case Format.R16G16Uint:
case Format.R16G16Float:
case Format.R11G11B10Float:
case Format.R32Sint:
case Format.R32Uint:
case Format.R32Float:
case Format.B5G6R5Unorm:
case Format.B5G5R5A1Unorm:
case Format.R8G8Unorm:
case Format.R8G8Snorm:
case Format.R8G8Sint:
case Format.R8G8Uint:
case Format.R16Unorm:
case Format.R16Snorm:
case Format.R16Sint:
case Format.R16Uint:
case Format.R16Float:
case Format.R8Unorm:
case Format.R8Snorm:
case Format.R8Sint:
case Format.R8Uint:
case Format.B5G5R5X1Unorm:
return true;
}
return false;
}
/// <summary>
/// Checks if the texture format is an ASTC format.
/// </summary>

View file

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ryujinx.Graphics.GAL
{
public struct HardwareInfo
{
public string GpuVendor { get; }
public string GpuModel { get; }
public HardwareInfo(string gpuVendor, string gpuModel)
{
GpuVendor = gpuVendor;
GpuModel = gpuModel;
}
}
}

View file

@ -1,3 +1,4 @@
using Ryujinx.Graphics.Shader;
using System;
namespace Ryujinx.Graphics.GAL
@ -79,15 +80,13 @@ namespace Ryujinx.Graphics.GAL
void SetRenderTargetColorMasks(ReadOnlySpan<uint> componentMask);
void SetRenderTargets(ITexture[] colors, ITexture depthStencil);
void SetSampler(int binding, ISampler sampler);
void SetScissor(int index, bool enable, int x, int y, int width, int height);
void SetScissors(ReadOnlySpan<Rectangle<int>> regions);
void SetStencilTest(StencilTestDescriptor stencilTest);
void SetStorageBuffers(int first, ReadOnlySpan<BufferRange> buffers);
void SetTexture(int binding, ITexture texture);
void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler);
void SetTransformFeedbackBuffers(ReadOnlySpan<BufferRange> buffers);
void SetUniformBuffers(int first, ReadOnlySpan<BufferRange> buffers);
@ -97,7 +96,7 @@ namespace Ryujinx.Graphics.GAL
void SetVertexAttribs(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs);
void SetVertexBuffers(ReadOnlySpan<VertexBufferDescriptor> vertexBuffers);
void SetViewports(int first, ReadOnlySpan<Viewport> viewports, bool disableTransform);
void SetViewports(ReadOnlySpan<Viewport> viewports, bool disableTransform);
void TextureBarrier();
void TextureBarrierTiled();

View file

@ -30,6 +30,7 @@ namespace Ryujinx.Graphics.GAL
ReadOnlySpan<byte> GetBufferData(BufferHandle buffer, int offset, int size);
Capabilities GetCapabilities();
HardwareInfo GetHardwareInfo();
IProgram LoadProgramBinary(byte[] programBinary, bool hasFragmentShader, ShaderInfo info);

View file

@ -1,6 +0,0 @@
using System;
namespace Ryujinx.Graphics.GAL
{
public interface IShader : IDisposable { }
}

View file

@ -201,14 +201,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
SetRenderTargetScaleCommand.Run(ref GetCommand<SetRenderTargetScaleCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.SetRenderTargets] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
SetRenderTargetsCommand.Run(ref GetCommand<SetRenderTargetsCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.SetSampler] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
SetSamplerCommand.Run(ref GetCommand<SetSamplerCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.SetScissor] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
SetScissorCommand.Run(ref GetCommand<SetScissorCommand>(memory), threaded, renderer);
SetScissorsCommand.Run(ref GetCommand<SetScissorsCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.SetStencilTest] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
SetStencilTestCommand.Run(ref GetCommand<SetStencilTestCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.SetTexture] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
SetTextureCommand.Run(ref GetCommand<SetTextureCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.SetTextureAndSampler] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
SetTextureAndSamplerCommand.Run(ref GetCommand<SetTextureAndSamplerCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.SetUserClipDistance] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
SetUserClipDistanceCommand.Run(ref GetCommand<SetUserClipDistanceCommand>(memory), threaded, renderer);
_lookup[(int)CommandType.SetVertexAttribs] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>

View file

@ -82,10 +82,9 @@
SetRenderTargetColorMasks,
SetRenderTargetScale,
SetRenderTargets,
SetSampler,
SetScissor,
SetStencilTest,
SetTexture,
SetTextureAndSampler,
SetUserClipDistance,
SetVertexAttribs,
SetVertexBuffers,

View file

@ -1,23 +0,0 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{
struct SetSamplerCommand : IGALCommand
{
public CommandType CommandType => CommandType.SetSampler;
private int _index;
private TableRef<ISampler> _sampler;
public void Set(int index, TableRef<ISampler> sampler)
{
_index = index;
_sampler = sampler;
}
public static void Run(ref SetSamplerCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
renderer.Pipeline.SetSampler(command._index, command._sampler.GetAs<ThreadedSampler>(threaded)?.Base);
}
}
}

View file

@ -1,28 +0,0 @@
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{
struct SetScissorCommand : IGALCommand
{
public CommandType CommandType => CommandType.SetScissor;
private int _index;
private bool _enable;
private int _x;
private int _y;
private int _width;
private int _height;
public void Set(int index, bool enable, int x, int y, int width, int height)
{
_index = index;
_enable = enable;
_x = x;
_y = y;
_width = width;
_height = height;
}
public static void Run(ref SetScissorCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
renderer.Pipeline.SetScissor(command._index, command._enable, command._x, command._y, command._width, command._height);
}
}
}

View file

@ -0,0 +1,22 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{
struct SetScissorsCommand : IGALCommand
{
public CommandType CommandType => CommandType.SetScissor;
private SpanRef<Rectangle<int>> _scissors;
public void Set(SpanRef<Rectangle<int>> scissors)
{
_scissors = scissors;
}
public static void Run(ref SetScissorsCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
renderer.Pipeline.SetScissors(command._scissors.Get(threaded));
command._scissors.Dispose(threaded);
}
}
}

View file

@ -0,0 +1,28 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{
struct SetTextureAndSamplerCommand : IGALCommand
{
public CommandType CommandType => CommandType.SetTextureAndSampler;
private ShaderStage _stage;
private int _binding;
private TableRef<ITexture> _texture;
private TableRef<ISampler> _sampler;
public void Set(ShaderStage stage, int binding, TableRef<ITexture> texture, TableRef<ISampler> sampler)
{
_stage = stage;
_binding = binding;
_texture = texture;
_sampler = sampler;
}
public static void Run(ref SetTextureAndSamplerCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
renderer.Pipeline.SetTextureAndSampler(command._stage, command._binding, command._texture.GetAs<ThreadedTexture>(threaded)?.Base, command._sampler.GetAs<ThreadedSampler>(threaded)?.Base);
}
}
}

View file

@ -1,23 +0,0 @@
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
namespace Ryujinx.Graphics.GAL.Multithreading.Commands
{
struct SetTextureCommand : IGALCommand
{
public CommandType CommandType => CommandType.SetTexture;
private int _binding;
private TableRef<ITexture> _texture;
public void Set(int binding, TableRef<ITexture> texture)
{
_binding = binding;
_texture = texture;
}
public static void Run(ref SetTextureCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
renderer.Pipeline.SetTexture(command._binding, command._texture.GetAs<ThreadedTexture>(threaded)?.Base);
}
}
}

View file

@ -7,13 +7,11 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands
struct SetViewportsCommand : IGALCommand
{
public CommandType CommandType => CommandType.SetViewports;
private int _first;
private SpanRef<Viewport> _viewports;
private bool _disableTransform;
public void Set(int first, SpanRef<Viewport> viewports, bool disableTransform)
public void Set(SpanRef<Viewport> viewports, bool disableTransform)
{
_first = first;
_viewports = viewports;
_disableTransform = disableTransform;
}
@ -21,7 +19,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Commands
public static void Run(ref SetViewportsCommand command, ThreadedRenderer threaded, IRenderer renderer)
{
ReadOnlySpan<Viewport> viewports = command._viewports.Get(threaded);
renderer.Pipeline.SetViewports(command._first, viewports, command._disableTransform);
renderer.Pipeline.SetViewports(viewports, command._disableTransform);
command._viewports.Dispose(threaded);
}
}

View file

@ -1,6 +1,7 @@
using Ryujinx.Graphics.GAL.Multithreading.Commands;
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
using Ryujinx.Graphics.Shader;
using System;
using System.Linq;
@ -250,15 +251,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_renderer.QueueCommand();
}
public void SetSampler(int binding, ISampler sampler)
public void SetScissors(ReadOnlySpan<Rectangle<int>> scissors)
{
_renderer.New<SetSamplerCommand>().Set(binding, Ref(sampler));
_renderer.QueueCommand();
}
public void SetScissor(int index, bool enable, int x, int y, int width, int height)
{
_renderer.New<SetScissorCommand>().Set(index, enable, x, y, width, height);
_renderer.New<SetScissorsCommand>().Set(_renderer.CopySpan(scissors));
_renderer.QueueCommand();
}
@ -274,9 +269,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_renderer.QueueCommand();
}
public void SetTexture(int binding, ITexture texture)
public void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler)
{
_renderer.New<SetTextureCommand>().Set(binding, Ref(texture));
_renderer.New<SetTextureAndSamplerCommand>().Set(stage, binding, Ref(texture), Ref(sampler));
_renderer.QueueCommand();
}
@ -310,9 +305,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_renderer.QueueCommand();
}
public void SetViewports(int first, ReadOnlySpan<Viewport> viewports, bool disableTransform)
public void SetViewports(ReadOnlySpan<Viewport> viewports, bool disableTransform)
{
_renderer.New<SetViewportsCommand>().Set(first, _renderer.CopySpan(viewports), disableTransform);
_renderer.New<SetViewportsCommand>().Set(_renderer.CopySpan(viewports), disableTransform);
_renderer.QueueCommand();
}

View file

@ -76,7 +76,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
renderer.ScreenCaptured += (object sender, ScreenCaptureImageInfo info) => ScreenCaptured?.Invoke(this, info);
Pipeline = new ThreadedPipeline(this, renderer.Pipeline);
Window = new ThreadedWindow(this, renderer.Window);
Window = new ThreadedWindow(this, renderer);
Buffers = new BufferMap();
Sync = new SyncMap();
Programs = new ProgramQueue(renderer);
@ -262,7 +262,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
{
var program = new ThreadedProgram(this);
SourceProgramRequest request = new SourceProgramRequest(program, shaders, info);
Programs.Add(request);
New<CreateProgramCommand>().Set(Ref((IProgramRequest)request));
@ -337,6 +339,11 @@ namespace Ryujinx.Graphics.GAL.Multithreading
return box.Result;
}
public HardwareInfo GetHardwareInfo()
{
return _baseRenderer.GetHardwareInfo();
}
/// <summary>
/// Initialize the base renderer. Must be called on the render thread.
/// </summary>

View file

@ -8,12 +8,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
public class ThreadedWindow : IWindow
{
private ThreadedRenderer _renderer;
private IWindow _impl;
private IRenderer _impl;
public ThreadedWindow(ThreadedRenderer renderer, IWindow window)
public ThreadedWindow(ThreadedRenderer renderer, IRenderer impl)
{
_renderer = renderer;
_impl = window;
_impl = impl;
}
public void Present(ITexture texture, ImageCrop crop, Action<object> swapBuffersCallback)
@ -28,7 +28,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
public void SetSize(int width, int height)
{
_impl.SetSize(width, height);
_impl.Window.SetSize(width, height);
}
}
}

View file

@ -0,0 +1,78 @@
using Ryujinx.Common.Memory;
using System;
namespace Ryujinx.Graphics.GAL
{
/// <summary>
/// Descriptor for a pipeline buffer binding.
/// </summary>
public struct BufferPipelineDescriptor
{
public bool Enable { get; }
public int Stride { get; }
public int Divisor { get; }
public BufferPipelineDescriptor(bool enable, int stride, int divisor)
{
Enable = enable;
Stride = stride;
Divisor = divisor;
}
}
/// <summary>
/// State required for a program to compile shaders.
/// </summary>
public struct ProgramPipelineState
{
// Some state is considered always dynamic and should not be included:
// - Viewports/Scissors
// - Bias values (not enable)
public int SamplesCount;
public Array8<bool> AttachmentEnable;
public Array8<Format> AttachmentFormats;
public bool DepthStencilEnable;
public Format DepthStencilFormat;
public bool LogicOpEnable;
public LogicalOp LogicOp;
public Array8<BlendDescriptor> BlendDescriptors;
public Array8<uint> ColorWriteMask;
public int VertexAttribCount;
public Array32<VertexAttribDescriptor> VertexAttribs;
public int VertexBufferCount;
public Array32<BufferPipelineDescriptor> VertexBuffers;
// TODO: Min/max depth bounds.
public DepthTestDescriptor DepthTest;
public StencilTestDescriptor StencilTest;
public FrontFace FrontFace;
public Face CullMode;
public bool CullEnable;
public PolygonModeMask BiasEnable;
public float LineWidth;
// TODO: Polygon mode.
public bool DepthClampEnable;
public bool RasterizerDiscard;
public PrimitiveTopology Topology;
public bool PrimitiveRestartEnable;
public uint PatchControlPoints;
public void SetVertexAttribs(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs)
{
VertexAttribCount = vertexAttribs.Length;
vertexAttribs.CopyTo(VertexAttribs.ToSpan());
}
public void SetLogicOpState(bool enable, LogicalOp op)
{
LogicOp = op;
LogicOpEnable = enable;
}
}
}

View file

@ -0,0 +1,18 @@
namespace Ryujinx.Graphics.GAL
{
public struct Rectangle<T> where T : unmanaged
{
public T X { get; }
public T Y { get; }
public T Width { get; }
public T Height { get; }
public Rectangle(T x, T y, T width, T height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
}
}

View file

@ -1,18 +0,0 @@
namespace Ryujinx.Graphics.GAL
{
public struct RectangleF
{
public float X { get; }
public float Y { get; }
public float Width { get; }
public float Height { get; }
public RectangleF(float x, float y, float width, float height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
}
}

View file

@ -50,5 +50,23 @@ namespace Ryujinx.Graphics.GAL
MipLodBias = mipLodBias;
MaxAnisotropy = maxAnisotropy;
}
public static SamplerCreateInfo Create(MinFilter minFilter, MagFilter magFilter)
{
return new SamplerCreateInfo(
minFilter,
magFilter,
false,
AddressMode.ClampToEdge,
AddressMode.ClampToEdge,
AddressMode.ClampToEdge,
CompareMode.None,
GAL.CompareOp.Always,
new ColorF(0f, 0f, 0f, 0f),
0f,
0f,
0f,
1f);
}
}
}

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace Ryujinx.Graphics.GAL
{
public struct ShaderBindings
{
public IReadOnlyCollection<int> UniformBufferBindings { get; }
public IReadOnlyCollection<int> StorageBufferBindings { get; }
public IReadOnlyCollection<int> TextureBindings { get; }
public IReadOnlyCollection<int> ImageBindings { get; }
public ShaderBindings(
IReadOnlyCollection<int> uniformBufferBindings,
IReadOnlyCollection<int> storageBufferBindings,
IReadOnlyCollection<int> textureBindings,
IReadOnlyCollection<int> imageBindings)
{
UniformBufferBindings = uniformBufferBindings;
StorageBufferBindings = storageBufferBindings;
TextureBindings = textureBindings;
ImageBindings = imageBindings;
}
}
}

View file

@ -3,10 +3,21 @@ namespace Ryujinx.Graphics.GAL
public struct ShaderInfo
{
public int FragmentOutputMap { get; }
public ProgramPipelineState? State { get; }
public bool FromCache { get; set; }
public ShaderInfo(int fragmentOutputMap)
public ShaderInfo(int fragmentOutputMap, ProgramPipelineState state, bool fromCache = false)
{
FragmentOutputMap = fragmentOutputMap;
State = state;
FromCache = fromCache;
}
public ShaderInfo(int fragmentOutputMap, bool fromCache = false)
{
FragmentOutputMap = fragmentOutputMap;
State = null;
FromCache = fromCache;
}
}
}

View file

@ -7,22 +7,24 @@ namespace Ryujinx.Graphics.GAL
{
public string Code { get; }
public byte[] BinaryCode { get; }
public ShaderBindings Bindings { get; }
public ShaderStage Stage { get; }
public TargetLanguage Language { get; }
public ShaderSource(string code, byte[] binaryCode, ShaderStage stage, TargetLanguage language)
public ShaderSource(string code, byte[] binaryCode, ShaderBindings bindings, ShaderStage stage, TargetLanguage language)
{
Code = code;
BinaryCode = binaryCode;
Bindings = bindings;
Stage = stage;
Language = language;
}
public ShaderSource(string code, ShaderStage stage, TargetLanguage language) : this(code, null, stage, language)
public ShaderSource(string code, ShaderBindings bindings, ShaderStage stage, TargetLanguage language) : this(code, null, bindings, stage, language)
{
}
public ShaderSource(byte[] binaryCode, ShaderStage stage, TargetLanguage language) : this(null, binaryCode, stage, language)
public ShaderSource(byte[] binaryCode, ShaderBindings bindings, ShaderStage stage, TargetLanguage language) : this(null, binaryCode, bindings, stage, language)
{
}
}

View file

@ -2,7 +2,7 @@ namespace Ryujinx.Graphics.GAL
{
public struct Viewport
{
public RectangleF Region { get; }
public Rectangle<float> Region { get; }
public ViewportSwizzle SwizzleX { get; }
public ViewportSwizzle SwizzleY { get; }
@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL
public float DepthFar { get; }
public Viewport(
RectangleF region,
Rectangle<float> region,
ViewportSwizzle swizzleX,
ViewportSwizzle swizzleY,
ViewportSwizzle swizzleZ,

View file

@ -18,7 +18,7 @@ namespace Ryujinx.Graphics.Gpu
/// Maximum number of compute storage buffers.
/// </summary>
/// <remarks>
/// The maximum number of storage buffers is API limited, the hardware supports a unlimited amount.
/// The maximum number of storage buffers is API limited, the hardware supports an unlimited amount.
/// </remarks>
public const int TotalCpStorageBuffers = 16;
@ -31,7 +31,7 @@ namespace Ryujinx.Graphics.Gpu
/// Maximum number of graphics storage buffers.
/// </summary>
/// <remarks>
/// The maximum number of storage buffers is API limited, the hardware supports a unlimited amount.
/// The maximum number of storage buffers is API limited, the hardware supports an unlimited amount.
/// </remarks>
public const int TotalGpStorageBuffers = 16;
@ -40,6 +40,22 @@ namespace Ryujinx.Graphics.Gpu
/// </summary>
public const int TotalTransformFeedbackBuffers = 4;
/// <summary>
/// Maximum number of textures on a single shader stage.
/// </summary>
/// <remarks>
/// The maximum number of textures is API limited, the hardware supports an unlimited amount.
/// </remarks>
public const int TotalTextures = 32;
/// <summary>
/// Maximum number of images on a single shader stage.
/// </summary>
/// <remarks>
/// The maximum number of images is API limited, the hardware supports an unlimited amount.
/// </remarks>
public const int TotalImages = 8;
/// <summary>
/// Maximum number of render target color buffers.
/// </summary>
@ -53,7 +69,7 @@ namespace Ryujinx.Graphics.Gpu
/// <summary>
/// Maximum number of vertex attributes.
/// </summary>
public const int TotalVertexAttribs = 16;
public const int TotalVertexAttribs = 16; // FIXME: Should be 32, but OpenGL only supports 16.
/// <summary>
/// Maximum number of vertex buffers.

View file

@ -147,6 +147,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Dma
int xCount = (int)_state.State.LineLengthIn;
int yCount = (int)_state.State.LineCount;
_3dEngine.CreatePendingSyncs();
_3dEngine.FlushUboDirty();
if (copy2D)

View file

@ -15,6 +15,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
private readonly GPFifoProcessor _parent;
private readonly DeviceState<GPFifoClassState> _state;
private int _previousSubChannel;
private bool _createSyncPending;
private const int MacrosCount = 0x80;
// Note: The size of the macro memory is unknown, we just make
@ -48,6 +51,18 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
_macroCode = new int[MacroCodeSize];
}
/// <summary>
/// Create any syncs from WaitForIdle command that are currently pending.
/// </summary>
public void CreatePendingSyncs()
{
if (_createSyncPending)
{
_createSyncPending = false;
_context.CreateHostSyncIfNeeded(false);
}
}
/// <summary>
/// Reads data from the class registers.
/// </summary>
@ -158,7 +173,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
_parent.PerformDeferredDraws();
_context.Renderer.Pipeline.Barrier();
_context.CreateHostSyncIfNeeded(false);
_createSyncPending = true;
}
/// <summary>

View file

@ -65,7 +65,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.GPFifo
_channel = channel;
_fifoClass = new GPFifoClass(context, this);
_3dClass = new ThreedClass(context, channel);
_3dClass = new ThreedClass(context, channel, _fifoClass);
_computeClass = new ComputeClass(context, channel, _3dClass);
_i2mClass = new InlineToMemoryClass(context, channel);
_2dClass = new TwodClass(channel);

View file

@ -559,7 +559,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
scissorH = (int)MathF.Ceiling(scissorH * scale);
}
_context.Renderer.Pipeline.SetScissor(0, true, scissorX, scissorY, scissorW, scissorH);
Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[]
{
new Rectangle<int>(scissorX, scissorY, scissorW, scissorH)
};
_context.Renderer.Pipeline.SetScissors(scissors);
}
if (clipMismatch)

View file

@ -1,4 +1,5 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.Types;
using Ryujinx.Graphics.Gpu.Image;
@ -15,11 +16,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
class StateUpdater
{
public const int ShaderStateIndex = 0;
public const int RasterizerStateIndex = 1;
public const int ScissorStateIndex = 2;
public const int VertexBufferStateIndex = 3;
public const int PrimitiveRestartStateIndex = 4;
public const int ShaderStateIndex = 16;
public const int RasterizerStateIndex = 15;
public const int ScissorStateIndex = 18;
public const int VertexBufferStateIndex = 0;
public const int PrimitiveRestartStateIndex = 12;
private readonly GpuContext _context;
private readonly GpuChannel _channel;
@ -31,6 +32,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private readonly ShaderProgramInfo[] _currentProgramInfo;
private ShaderSpecializationState _shaderSpecState;
private ProgramPipelineState _pipeline;
private bool _vtgWritesRtLayer;
private byte _vsClipDistancesWritten;
@ -54,7 +57,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_drawState = drawState;
_currentProgramInfo = new ShaderProgramInfo[Constants.ShaderStages];
// ShaderState must be the first, as other state updates depends on information from the currently bound shader.
// ShaderState must be updated after other state updates, as pipeline state is sent to the backend when compiling new shaders.
// Render target state must appear after shader state as it depends on information from the currently bound shader.
// Rasterizer and scissor states are checked by render target clear, their indexes
// must be updated on the constants "RasterizerStateIndex" and "ScissorStateIndex" if modified.
// The vertex buffer state may be forced dirty when a indexed draw starts, the "VertexBufferStateIndex"
@ -62,53 +66,39 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
// The order of the other state updates doesn't matter.
_updateTracker = new StateUpdateTracker<ThreedClassState>(new[]
{
new StateUpdateCallbackEntry(UpdateShaderState,
nameof(ThreedClassState.ShaderBaseAddress),
nameof(ThreedClassState.ShaderState)),
new StateUpdateCallbackEntry(UpdateRasterizerState, nameof(ThreedClassState.RasterizeEnable)),
new StateUpdateCallbackEntry(UpdateScissorState,
nameof(ThreedClassState.ScissorState),
nameof(ThreedClassState.ScreenScissorState)),
new StateUpdateCallbackEntry(UpdateVertexBufferState,
nameof(ThreedClassState.VertexBufferDrawState),
nameof(ThreedClassState.VertexBufferInstanced),
nameof(ThreedClassState.VertexBufferState),
nameof(ThreedClassState.VertexBufferEndAddress)),
new StateUpdateCallbackEntry(UpdatePrimitiveRestartState,
nameof(ThreedClassState.PrimitiveRestartDrawArrays),
nameof(ThreedClassState.PrimitiveRestartState)),
new StateUpdateCallbackEntry(UpdateVertexAttribState, nameof(ThreedClassState.VertexAttribState)),
new StateUpdateCallbackEntry(UpdateTessellationState,
nameof(ThreedClassState.TessOuterLevel),
nameof(ThreedClassState.TessInnerLevel),
nameof(ThreedClassState.PatchVertices)),
new StateUpdateCallbackEntry(UpdateBlendState,
nameof(ThreedClassState.BlendIndependent),
nameof(ThreedClassState.BlendConstant),
nameof(ThreedClassState.BlendStateCommon),
nameof(ThreedClassState.BlendEnableCommon),
nameof(ThreedClassState.BlendEnable),
nameof(ThreedClassState.BlendState)),
new StateUpdateCallbackEntry(UpdateTfBufferState, nameof(ThreedClassState.TfBufferState)),
new StateUpdateCallbackEntry(UpdateUserClipState, nameof(ThreedClassState.ClipDistanceEnable)),
new StateUpdateCallbackEntry(UpdateFaceState, nameof(ThreedClassState.FaceState)),
new StateUpdateCallbackEntry(UpdateRenderTargetState,
nameof(ThreedClassState.RtColorState),
nameof(ThreedClassState.RtDepthStencilState),
nameof(ThreedClassState.RtControl),
nameof(ThreedClassState.RtDepthStencilSize),
nameof(ThreedClassState.RtDepthStencilEnable)),
new StateUpdateCallbackEntry(UpdateDepthClampState, nameof(ThreedClassState.ViewVolumeClipControl)),
new StateUpdateCallbackEntry(UpdateAlphaTestState,
nameof(ThreedClassState.AlphaTestEnable),
nameof(ThreedClassState.AlphaTestRef),
nameof(ThreedClassState.AlphaTestFunc)),
new StateUpdateCallbackEntry(UpdateStencilTestState,
nameof(ThreedClassState.StencilBackMasks),
nameof(ThreedClassState.StencilTestState),
nameof(ThreedClassState.StencilBackTestState)),
new StateUpdateCallbackEntry(UpdateDepthTestState,
nameof(ThreedClassState.DepthTestEnable),
nameof(ThreedClassState.DepthWriteEnable),
nameof(ThreedClassState.DepthTestFunc)),
new StateUpdateCallbackEntry(UpdateTessellationState,
nameof(ThreedClassState.TessOuterLevel),
nameof(ThreedClassState.TessInnerLevel),
nameof(ThreedClassState.PatchVertices)),
new StateUpdateCallbackEntry(UpdateViewportTransform,
nameof(ThreedClassState.DepthMode),
nameof(ThreedClassState.ViewportTransform),
@ -116,6 +106,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
nameof(ThreedClassState.YControl),
nameof(ThreedClassState.ViewportTransformEnable)),
new StateUpdateCallbackEntry(UpdateLogicOpState, nameof(ThreedClassState.LogicOpState)),
new StateUpdateCallbackEntry(UpdateDepthClampState, nameof(ThreedClassState.ViewVolumeClipControl)),
new StateUpdateCallbackEntry(UpdatePolygonMode,
nameof(ThreedClassState.PolygonModeFront),
nameof(ThreedClassState.PolygonModeBack)),
@ -126,21 +120,46 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
nameof(ThreedClassState.DepthBiasUnits),
nameof(ThreedClassState.DepthBiasClamp)),
new StateUpdateCallbackEntry(UpdateStencilTestState,
nameof(ThreedClassState.StencilBackMasks),
nameof(ThreedClassState.StencilTestState),
nameof(ThreedClassState.StencilBackTestState)),
new StateUpdateCallbackEntry(UpdatePrimitiveRestartState, nameof(ThreedClassState.PrimitiveRestartState)),
new StateUpdateCallbackEntry(UpdateLineState,
nameof(ThreedClassState.LineWidthSmooth),
nameof(ThreedClassState.LineSmoothEnable)),
new StateUpdateCallbackEntry(UpdateRtColorMask,
nameof(ThreedClassState.RtColorMaskShared),
nameof(ThreedClassState.RtColorMask)),
new StateUpdateCallbackEntry(UpdateRasterizerState, nameof(ThreedClassState.RasterizeEnable)),
new StateUpdateCallbackEntry(UpdateShaderState,
nameof(ThreedClassState.ShaderBaseAddress),
nameof(ThreedClassState.ShaderState)),
new StateUpdateCallbackEntry(UpdateRenderTargetState,
nameof(ThreedClassState.RtColorState),
nameof(ThreedClassState.RtDepthStencilState),
nameof(ThreedClassState.RtControl),
nameof(ThreedClassState.RtDepthStencilSize),
nameof(ThreedClassState.RtDepthStencilEnable)),
new StateUpdateCallbackEntry(UpdateScissorState,
nameof(ThreedClassState.ScissorState),
nameof(ThreedClassState.ScreenScissorState)),
new StateUpdateCallbackEntry(UpdateTfBufferState, nameof(ThreedClassState.TfBufferState)),
new StateUpdateCallbackEntry(UpdateUserClipState, nameof(ThreedClassState.ClipDistanceEnable)),
new StateUpdateCallbackEntry(UpdateAlphaTestState,
nameof(ThreedClassState.AlphaTestEnable),
nameof(ThreedClassState.AlphaTestRef),
nameof(ThreedClassState.AlphaTestFunc)),
new StateUpdateCallbackEntry(UpdateSamplerPoolState,
nameof(ThreedClassState.SamplerPoolState),
nameof(ThreedClassState.SamplerIndex)),
new StateUpdateCallbackEntry(UpdateTexturePoolState, nameof(ThreedClassState.TexturePoolState)),
new StateUpdateCallbackEntry(UpdateVertexAttribState, nameof(ThreedClassState.VertexAttribState)),
new StateUpdateCallbackEntry(UpdateLineState,
nameof(ThreedClassState.LineWidthSmooth),
nameof(ThreedClassState.LineSmoothEnable)),
new StateUpdateCallbackEntry(UpdatePointState,
nameof(ThreedClassState.PointSize),
@ -152,22 +171,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
nameof(ThreedClassState.IndexBufferState),
nameof(ThreedClassState.IndexBufferCount)),
new StateUpdateCallbackEntry(UpdateFaceState, nameof(ThreedClassState.FaceState)),
new StateUpdateCallbackEntry(UpdateRtColorMask,
nameof(ThreedClassState.RtColorMaskShared),
nameof(ThreedClassState.RtColorMask)),
new StateUpdateCallbackEntry(UpdateBlendState,
nameof(ThreedClassState.BlendIndependent),
nameof(ThreedClassState.BlendConstant),
nameof(ThreedClassState.BlendStateCommon),
nameof(ThreedClassState.BlendEnableCommon),
nameof(ThreedClassState.BlendEnable),
nameof(ThreedClassState.BlendState)),
new StateUpdateCallbackEntry(UpdateLogicOpState, nameof(ThreedClassState.LogicOpState)),
new StateUpdateCallbackEntry(UpdateMultisampleState,
nameof(ThreedClassState.AlphaToCoverageDitherEnable),
nameof(ThreedClassState.MultisampleControl))
@ -324,6 +327,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
private void UpdateTessellationState()
{
_pipeline.PatchControlPoints = (uint)_state.State.PatchVertices;
_context.Renderer.Pipeline.SetPatchParameters(
_state.State.PatchVertices,
_state.State.TessOuterLevel.ToSpan(),
@ -356,6 +361,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private void UpdateRasterizerState()
{
bool enable = _state.State.RasterizeEnable;
_pipeline.RasterizerDiscard = !enable;
_context.Renderer.Pipeline.SetRasterizerDiscard(!enable);
}
@ -497,11 +503,21 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
public void UpdateScissorState()
{
const int MinX = 0;
const int MinY = 0;
const int MaxW = 0xffff;
const int MaxH = 0xffff;
Span<Rectangle<int>> regions = stackalloc Rectangle<int>[Constants.TotalViewports];
for (int index = 0; index < Constants.TotalViewports; index++)
{
ScissorState scissor = _state.State.ScissorState[index];
bool enable = scissor.Enable && (scissor.X1 != 0 || scissor.Y1 != 0 || scissor.X2 != 0xffff || scissor.Y2 != 0xffff);
bool enable = scissor.Enable && (scissor.X1 != MinX ||
scissor.Y1 != MinY ||
scissor.X2 != MaxW ||
scissor.Y2 != MaxH);
if (enable)
{
@ -531,13 +547,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
height = (int)MathF.Ceiling(height * scale);
}
_context.Renderer.Pipeline.SetScissor(index, true, x, y, width, height);
regions[index] = new Rectangle<int>(x, y, width, height);
}
else
{
_context.Renderer.Pipeline.SetScissor(index, false, 0, 0, 0, 0);
regions[index] = new Rectangle<int>(MinX, MinY, MaxW, MaxH);
}
}
_context.Renderer.Pipeline.SetScissors(regions);
}
/// <summary>
@ -547,7 +565,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
private void UpdateDepthClampState()
{
ViewVolumeClipControl clip = _state.State.ViewVolumeClipControl;
_context.Renderer.Pipeline.SetDepthClamp((clip & ViewVolumeClipControl.DepthClampDisabled) == 0);
bool clamp = (clip & ViewVolumeClipControl.DepthClampDisabled) == 0;
_pipeline.DepthClampEnable = clamp;
_context.Renderer.Pipeline.SetDepthClamp(clamp);
}
/// <summary>
@ -566,10 +587,13 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
private void UpdateDepthTestState()
{
_context.Renderer.Pipeline.SetDepthTest(new DepthTestDescriptor(
DepthTestDescriptor descriptor = new DepthTestDescriptor(
_state.State.DepthTestEnable,
_state.State.DepthWriteEnable,
_state.State.DepthTestFunc));
_state.State.DepthTestFunc);
_pipeline.DepthTest = descriptor;
_context.Renderer.Pipeline.SetDepthTest(descriptor);
}
/// <summary>
@ -596,7 +620,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
ref var scissor = ref _state.State.ScreenScissorState;
float rScale = _channel.TextureManager.RenderTargetScale;
var scissorRect = new RectangleF(0, 0, (scissor.X + scissor.Width) * rScale, (scissor.Y + scissor.Height) * rScale);
var scissorRect = new Rectangle<float>(0, 0, (scissor.X + scissor.Width) * rScale, (scissor.Y + scissor.Height) * rScale);
viewports[index] = new Viewport(scissorRect, ViewportSwizzle.PositiveX, ViewportSwizzle.PositiveY, ViewportSwizzle.PositiveZ, ViewportSwizzle.PositiveW, 0, 1);
continue;
@ -633,7 +657,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
height *= scale;
}
RectangleF region = new RectangleF(x, y, width, height);
Rectangle<float> region = new Rectangle<float>(x, y, width, height);
ViewportSwizzle swizzleX = transform.UnpackSwizzleX();
ViewportSwizzle swizzleY = transform.UnpackSwizzleY();
@ -653,7 +677,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
viewports[index] = new Viewport(region, swizzleX, swizzleY, swizzleZ, swizzleW, depthNear, depthFar);
}
_context.Renderer.Pipeline.SetViewports(0, viewports, disableTransform);
_context.Renderer.Pipeline.SetDepthMode(GetDepthMode());
_context.Renderer.Pipeline.SetViewports(viewports, disableTransform);
}
/// <summary>
@ -661,37 +686,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
private void UpdateDepthMode()
{
ref var transform = ref _state.State.ViewportTransform[0];
ref var extents = ref _state.State.ViewportExtents[0];
DepthMode depthMode;
if (!float.IsInfinity(extents.DepthNear) &&
!float.IsInfinity(extents.DepthFar) &&
(extents.DepthFar - extents.DepthNear) != 0)
{
// Try to guess the depth mode being used on the high level API
// based on current transform.
// It is setup like so by said APIs:
// If depth mode is ZeroToOne:
// TranslateZ = Near
// ScaleZ = Far - Near
// If depth mode is MinusOneToOne:
// TranslateZ = (Near + Far) / 2
// ScaleZ = (Far - Near) / 2
// DepthNear/Far are sorted such as that Near is always less than Far.
depthMode = extents.DepthNear != transform.TranslateZ &&
extents.DepthFar != transform.TranslateZ
? DepthMode.MinusOneToOne
: DepthMode.ZeroToOne;
}
else
{
// If we can't guess from the viewport transform, then just use the depth mode register.
depthMode = (DepthMode)(_state.State.DepthMode & 1);
}
_context.Renderer.Pipeline.SetDepthMode(depthMode);
_context.Renderer.Pipeline.SetDepthMode(GetDepthMode());
}
/// <summary>
@ -719,6 +714,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
enables |= (depthBias.LineEnable ? PolygonModeMask.Line : 0);
enables |= (depthBias.FillEnable ? PolygonModeMask.Fill : 0);
_pipeline.BiasEnable = enables;
_context.Renderer.Pipeline.SetDepthBias(enables, factor, units / 2f, clamp);
}
@ -760,7 +756,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
backMask = test.FrontMask;
}
_context.Renderer.Pipeline.SetStencilTest(new StencilTestDescriptor(
StencilTestDescriptor descriptor = new StencilTestDescriptor(
test.Enable,
test.FrontFunc,
test.FrontSFail,
@ -775,7 +771,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
backDpFail,
backFuncRef,
backFuncMask,
backMask));
backMask);
_pipeline.StencilTest = descriptor;
_context.Renderer.Pipeline.SetStencilTest(descriptor);
}
/// <summary>
@ -844,6 +843,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
format);
}
_pipeline.SetVertexAttribs(vertexAttribs);
_context.Renderer.Pipeline.SetVertexAttribs(vertexAttribs);
}
@ -855,6 +855,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
float width = _state.State.LineWidthSmooth;
bool smooth = _state.State.LineSmoothEnable;
_pipeline.LineWidth = width;
_context.Renderer.Pipeline.SetLineParameters(width, smooth);
}
@ -881,6 +882,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
PrimitiveRestartState primitiveRestart = _state.State.PrimitiveRestartState;
bool enable = primitiveRestart.Enable && (_drawState.DrawIndexed || _state.State.PrimitiveRestartDrawArrays);
_pipeline.PrimitiveRestartEnable = enable;
_context.Renderer.Pipeline.SetPrimitiveRestart(enable, primitiveRestart.Index);
}
@ -927,6 +929,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (!vertexBuffer.UnpackEnable())
{
_pipeline.VertexBuffers[index] = new BufferPipelineDescriptor(false, 0, 0);
_channel.BufferManager.SetVertexBuffer(index, 0, 0, 0, 0);
continue;
@ -944,6 +947,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_drawState.IsAnyVbInstanced |= divisor != 0;
ulong vbSize = endAddress.Pack() - address + 1;
ulong size;
if (_drawState.IbStreamer.HasInlineIndexData || _drawState.DrawIndexed || stride == 0 || instanced)
@ -951,7 +955,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
// This size may be (much) larger than the real vertex buffer size.
// Avoid calculating it this way, unless we don't have any other option.
size = endAddress.Pack() - address + 1;
size = vbSize;
if (stride > 0 && indexTypeSmall && _drawState.DrawIndexed && !instanced)
{
@ -975,9 +979,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
var drawState = _state.State.VertexBufferDrawState;
size = (ulong)((firstInstance + drawState.First + drawState.Count) * stride);
size = Math.Min(vbSize, (ulong)((firstInstance + drawState.First + drawState.Count) * stride));
}
_pipeline.VertexBuffers[index] = new BufferPipelineDescriptor(_channel.MemoryManager.IsMapped(address), stride, divisor);
_channel.BufferManager.SetVertexBuffer(index, address, size, stride, divisor);
}
}
@ -990,6 +995,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
var yControl = _state.State.YControl;
var face = _state.State.FaceState;
_pipeline.CullEnable = face.CullEnable;
_pipeline.CullMode = face.CullFace;
_context.Renderer.Pipeline.SetFaceCulling(face.CullEnable, face.CullFace);
UpdateFrontFace(yControl, face.FrontFace);
@ -1009,6 +1016,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
frontFace = frontFace == FrontFace.CounterClockwise ? FrontFace.Clockwise : FrontFace.CounterClockwise;
}
_pipeline.FrontFace = frontFace;
_context.Renderer.Pipeline.SetFrontFace(frontFace);
}
@ -1034,6 +1042,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
componentMask |= (colorMask.UnpackAlpha() ? 8u : 0u);
componentMasks[index] = componentMask;
_pipeline.ColorWriteMask[index] = componentMask;
}
_context.Renderer.Pipeline.SetRenderTargetColorMasks(componentMasks);
@ -1082,6 +1091,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
blend.AlphaDstFactor);
}
_pipeline.BlendDescriptors[index] = descriptor;
_context.Renderer.Pipeline.SetBlendState(index, descriptor);
}
}
@ -1093,6 +1103,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
LogicalOpState logicOpState = _state.State.LogicOpState;
_pipeline.SetLogicOpState(logicOpState.Enable, logicOpState.LogicalOp);
_context.Renderer.Pipeline.SetLogicOpState(logicOpState.Enable, logicOpState.LogicalOp);
}
@ -1138,7 +1149,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
GpuChannelPoolState poolState = GetPoolState();
GpuChannelGraphicsState graphicsState = GetGraphicsState();
CachedShaderProgram gs = shaderCache.GetGraphicsShader(ref _state.State, _channel, poolState, graphicsState, addresses);
CachedShaderProgram gs = shaderCache.GetGraphicsShader(ref _state.State, ref _pipeline, _channel, poolState, graphicsState, addresses);
_shaderSpecState = gs.SpecializationState;
@ -1245,13 +1256,69 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// <returns>Current GPU channel state</returns>
private GpuChannelGraphicsState GetGraphicsState()
{
ref var vertexAttribState = ref _state.State.VertexAttribState;
Array32<AttributeType> attributeTypes = new Array32<AttributeType>();
for (int location = 0; location < attributeTypes.Length; location++)
{
attributeTypes[location] = vertexAttribState[location].UnpackType() switch
{
3 => AttributeType.Sint,
4 => AttributeType.Uint,
_ => AttributeType.Float
};
}
return new GpuChannelGraphicsState(
_state.State.EarlyZForce,
_drawState.Topology,
_state.State.TessMode,
_state.State.ViewportTransformEnable == 0,
(_state.State.MultisampleControl & 1) != 0,
_state.State.AlphaToCoverageDitherEnable);
_state.State.AlphaToCoverageDitherEnable,
_state.State.ViewportTransformEnable == 0,
GetDepthMode() == DepthMode.MinusOneToOne,
_state.State.VertexProgramPointSize,
_state.State.PointSize,
_state.State.AlphaTestEnable,
_state.State.AlphaTestFunc,
_state.State.AlphaTestRef,
ref attributeTypes);
}
private DepthMode GetDepthMode()
{
ref var transform = ref _state.State.ViewportTransform[0];
ref var extents = ref _state.State.ViewportExtents[0];
DepthMode depthMode;
if (!float.IsInfinity(extents.DepthNear) &&
!float.IsInfinity(extents.DepthFar) &&
(extents.DepthFar - extents.DepthNear) != 0)
{
// Try to guess the depth mode being used on the high level API
// based on current transform.
// It is setup like so by said APIs:
// If depth mode is ZeroToOne:
// TranslateZ = Near
// ScaleZ = Far - Near
// If depth mode is MinusOneToOne:
// TranslateZ = (Near + Far) / 2
// ScaleZ = (Far - Near) / 2
// DepthNear/Far are sorted such as that Near is always less than Far.
depthMode = extents.DepthNear != transform.TranslateZ &&
extents.DepthFar != transform.TranslateZ
? DepthMode.MinusOneToOne
: DepthMode.ZeroToOne;
}
else
{
// If we can't guess from the viewport transform, then just use the depth mode register.
depthMode = (DepthMode)(_state.State.DepthMode & 1);
}
return depthMode;
}
/// <summary>

View file

@ -1,5 +1,6 @@
using Ryujinx.Graphics.Device;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.GPFifo;
using Ryujinx.Graphics.Gpu.Engine.InlineToMemory;
using System;
using System.Collections.Generic;
@ -13,6 +14,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
class ThreedClass : IDeviceState
{
private readonly GpuContext _context;
private readonly GPFifoClass _fifoClass;
private readonly DeviceStateWithShadow<ThreedClassState> _state;
private readonly InlineToMemoryClass _i2mClass;
@ -26,9 +28,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
/// <param name="context">GPU context</param>
/// <param name="channel">GPU channel</param>
public ThreedClass(GpuContext context, GpuChannel channel)
public ThreedClass(GpuContext context, GpuChannel channel, GPFifoClass fifoClass)
{
_context = context;
_fifoClass = fifoClass;
_state = new DeviceStateWithShadow<ThreedClassState>(new Dictionary<string, RwCallback>
{
{ nameof(ThreedClassState.LaunchDma), new RwCallback(LaunchDma, null) },
@ -114,6 +117,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
/// </summary>
public void UpdateState()
{
_fifoClass.CreatePendingSyncs();
_cbUpdater.FlushUboDirty();
_stateUpdater.Update();
}
@ -172,6 +176,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
_stateUpdater.ForceShaderUpdate();
}
/// <summary>
/// Create any syncs from WaitForIdle command that are currently pending.
/// </summary>
public void CreatePendingSyncs()
{
_fifoClass.CreatePendingSyncs();
}
/// <summary>
/// Flushes any queued UBO updates.
/// </summary>

View file

@ -311,6 +311,15 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
{
return Attribute & 0x3fe00000;
}
/// <summary>
/// Unpacks the Maxwell attribute component type.
/// </summary>
/// <returns>Attribute component type</returns>
public uint UnpackType()
{
return (Attribute >> 27) & 7;
}
}
/// <summary>
@ -759,8 +768,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
public fixed uint Reserved10B0[18];
public uint ClearFlags;
public fixed uint Reserved10FC[25];
public Array16<VertexAttribState> VertexAttribState;
public fixed uint Reserved11A0[31];
public Array32<VertexAttribState> VertexAttribState;
public fixed uint Reserved11E0[15];
public RtControl RtControl;
public fixed uint Reserved1220[2];
public Size3D RtDepthStencilSize;

View file

@ -56,5 +56,15 @@ namespace Ryujinx.Graphics.Gpu
/// Enables or disables the shader cache.
/// </summary>
public static bool EnableShaderCache;
/// <summary>
/// Enables or disables shader SPIR-V compilation.
/// </summary>
public static bool EnableSpirvCompilationOnVulkan = true;
/// <summary>
/// Enables or disables recompression of compressed textures that are not natively supported by the host.
/// </summary>
public static bool EnableTextureRecompression = false;
}
}

View file

@ -826,20 +826,25 @@ namespace Ryujinx.Graphics.Gpu.Image
depth,
levels,
layers,
out Span<byte> decoded))
out byte[] decoded))
{
string texInfo = $"{Info.Target} {Info.FormatInfo.Format} {Info.Width}x{Info.Height}x{Info.DepthOrLayers} levels {Info.Levels}";
Logger.Debug?.Print(LogClass.Gpu, $"Invalid ASTC texture at 0x{Info.GpuAddress:X} ({texInfo}).");
}
if (GraphicsConfig.EnableTextureRecompression)
{
decoded = BCnEncoder.EncodeBC7(decoded, width, height, depth, levels, layers);
}
data = decoded;
}
else if (!_context.Capabilities.SupportsR4G4Format && Format == Format.R4G4Unorm)
{
data = PixelConverter.ConvertR4G4ToR4G4B4A4(data);
}
else if (!_context.Capabilities.Supports3DTextureCompression && Target == Target.Texture3D)
else if (!TextureCompatibility.HostSupportsBcFormat(Format, Target, _context.Capabilities))
{
switch (Format)
{
@ -863,6 +868,14 @@ namespace Ryujinx.Graphics.Gpu.Image
case Format.Bc5Unorm:
data = BCnDecoder.DecodeBC5(data, width, height, depth, levels, layers, Format == Format.Bc5Snorm);
break;
case Format.Bc6HSfloat:
case Format.Bc6HUfloat:
data = BCnDecoder.DecodeBC6(data, width, height, depth, levels, layers, Format == Format.Bc6HSfloat);
break;
case Format.Bc7Srgb:
case Format.Bc7Unorm:
data = BCnDecoder.DecodeBC7(data, width, height, depth, levels, layers);
break;
}
}
@ -1151,7 +1164,7 @@ namespace Ryujinx.Graphics.Gpu.Image
result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewFormatCompatible(Info, info, caps));
if (result != TextureViewCompatibility.Incompatible)
{
result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info));
result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info, ref caps));
bool bothMs = Info.Target.IsMultisample() && info.Target.IsMultisample();
if (bothMs && (Info.SamplesInX != info.SamplesInX || Info.SamplesInY != info.SamplesInY))
@ -1216,16 +1229,18 @@ namespace Ryujinx.Graphics.Gpu.Image
if (_arrayViewTexture == null && IsSameDimensionsTarget(target))
{
FormatInfo formatInfo = TextureCompatibility.ToHostCompatibleFormat(Info, _context.Capabilities);
TextureCreateInfo createInfo = new TextureCreateInfo(
Info.Width,
Info.Height,
target == Target.CubemapArray ? 6 : 1,
Info.Levels,
Info.Samples,
Info.FormatInfo.BlockWidth,
Info.FormatInfo.BlockHeight,
Info.FormatInfo.BytesPerPixel,
Info.FormatInfo.Format,
formatInfo.BlockWidth,
formatInfo.BlockHeight,
formatInfo.BytesPerPixel,
formatInfo.Format,
Info.DepthStencilMode,
target,
Info.SwizzleR,

View file

@ -280,6 +280,30 @@ namespace Ryujinx.Graphics.Gpu.Image
return changed;
}
/// <summary>
/// Determines if the vertex stage requires a scale value.
/// </summary>
private bool VertexRequiresScale()
{
for (int i = 0; i < _textureBindingsCount[0]; i++)
{
if ((_textureBindings[0][i].Flags & TextureUsageFlags.NeedsScaleValue) != 0)
{
return true;
}
}
for (int i = 0; i < _imageBindingsCount[0]; i++)
{
if ((_imageBindings[0][i].Flags & TextureUsageFlags.NeedsScaleValue) != 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Uploads texture and image scales to the backend when they are used.
/// </summary>
@ -291,10 +315,10 @@ namespace Ryujinx.Graphics.Gpu.Image
int fragmentIndex = (int)ShaderStage.Fragment - 1;
int fragmentTotal = _isCompute ? 0 : (_textureBindingsCount[fragmentIndex] + _imageBindingsCount[fragmentIndex]);
if (total != 0 && fragmentTotal != _lastFragmentTotal)
if (total != 0 && fragmentTotal != _lastFragmentTotal && VertexRequiresScale())
{
// Must update scales in the support buffer if:
// - Vertex stage has bindings.
// - Vertex stage has bindings that require scale.
// - Fragment stage binding count has been updated since last render scale update.
_scaleChanged = true;
@ -420,6 +444,25 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// Counts the total number of texture bindings used by all shader stages.
/// </summary>
/// <returns>The total amount of textures used</returns>
private int GetTextureBindingsCount()
{
int count = 0;
for (int i = 0; i < _textureBindings.Length; i++)
{
if (_textureBindings[i] != null)
{
count += _textureBindings[i].Length;
}
}
return count;
}
/// <summary>
/// Ensures that the texture bindings are visible to the host GPU.
/// Note: this actually performs the binding using the host graphics API.
@ -501,7 +544,7 @@ namespace Ryujinx.Graphics.Gpu.Image
state.ScaleIndex = index;
state.UsageFlags = usageFlags;
_context.Renderer.Pipeline.SetTexture(bindingInfo.Binding, hostTextureRebind);
_context.Renderer.Pipeline.SetTextureAndSampler(stage, bindingInfo.Binding, hostTextureRebind, state.Sampler);
}
continue;
@ -514,44 +557,42 @@ namespace Ryujinx.Graphics.Gpu.Image
specStateMatches &= specState.MatchesTexture(stage, index, descriptor);
Sampler sampler = _samplerPool?.Get(samplerId);
ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target);
ISampler hostSampler = sampler?.GetHostSampler(texture);
if (hostTexture != null && texture.Target == Target.TextureBuffer)
{
// Ensure that the buffer texture is using the correct buffer as storage.
// Buffers are frequently re-created to accomodate larger data, so we need to re-bind
// to ensure we're not using a old buffer that was already deleted.
_channel.BufferManager.SetBufferTextureStorage(hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, bindingInfo.Format, false);
_channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, bindingInfo.Format, false);
}
else
{
if (state.Texture != hostTexture)
{
if (UpdateScale(texture, usageFlags, index, stage))
bool textureOrSamplerChanged = state.Texture != hostTexture || state.Sampler != hostSampler;
if ((state.ScaleIndex != index || state.UsageFlags != usageFlags || textureOrSamplerChanged) &&
UpdateScale(texture, usageFlags, index, stage))
{
hostTexture = texture?.GetTargetTexture(bindingInfo.Target);
textureOrSamplerChanged = true;
}
if (textureOrSamplerChanged)
{
state.Texture = hostTexture;
state.ScaleIndex = index;
state.UsageFlags = usageFlags;
_context.Renderer.Pipeline.SetTexture(bindingInfo.Binding, hostTexture);
}
Sampler sampler = samplerPool?.Get(samplerId);
state.CachedSampler = sampler;
ISampler hostSampler = sampler?.GetHostSampler(texture);
if (state.Sampler != hostSampler)
{
state.Sampler = hostSampler;
_context.Renderer.Pipeline.SetSampler(bindingInfo.Binding, hostSampler);
_context.Renderer.Pipeline.SetTextureAndSampler(stage, bindingInfo.Binding, hostTexture, hostSampler);
}
state.CachedTexture = texture;
state.CachedSampler = sampler;
state.InvalidatedSequence = texture?.InvalidatedSequence ?? 0;
}
}
@ -625,7 +666,7 @@ namespace Ryujinx.Graphics.Gpu.Image
cachedTexture?.SignalModified();
}
if ((state.ScaleIndex != index || state.UsageFlags != usageFlags) &&
if ((state.ScaleIndex != scaleIndex || state.UsageFlags != usageFlags) &&
UpdateScale(state.CachedTexture, usageFlags, scaleIndex, stage))
{
ITexture hostTextureRebind = state.CachedTexture.GetTargetTexture(bindingInfo.Target);
@ -663,7 +704,7 @@ namespace Ryujinx.Graphics.Gpu.Image
format = texture.Format;
}
_channel.BufferManager.SetBufferTextureStorage(hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, format, true);
_channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, format, true);
}
else
{
@ -672,13 +713,14 @@ namespace Ryujinx.Graphics.Gpu.Image
texture?.SignalModified();
}
if (state.Texture != hostTexture)
{
if (UpdateScale(texture, usageFlags, scaleIndex, stage))
if ((state.ScaleIndex != scaleIndex || state.UsageFlags != usageFlags || state.Texture != hostTexture) &&
UpdateScale(texture, usageFlags, scaleIndex, stage))
{
hostTexture = texture?.GetTargetTexture(bindingInfo.Target);
}
if (state.Texture != hostTexture)
{
state.Texture = hostTexture;
state.ScaleIndex = scaleIndex;
state.UsageFlags = usageFlags;

View file

@ -71,11 +71,15 @@ namespace Ryujinx.Graphics.Gpu.Image
{
if (info.FormatInfo.Format.IsAstcUnorm())
{
return new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4);
return GraphicsConfig.EnableTextureRecompression
? new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4)
: new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4);
}
else if (info.FormatInfo.Format.IsAstcSrgb())
{
return new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4);
return GraphicsConfig.EnableTextureRecompression
? new FormatInfo(Format.Bc7Srgb, 4, 4, 16, 4)
: new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4);
}
}
@ -84,9 +88,9 @@ namespace Ryujinx.Graphics.Gpu.Image
return new FormatInfo(Format.R4G4B4A4Unorm, 1, 1, 2, 4);
}
if (!caps.Supports3DTextureCompression && info.Target == Target.Texture3D)
if (!HostSupportsBcFormat(info.FormatInfo.Format, info.Target, caps))
{
// The host API does not support 3D compressed formats.
// The host API does not this compressed format.
// We assume software decompression will be done for those textures,
// and so we adjust the format here to match the decompressor output.
switch (info.FormatInfo.Format)
@ -94,10 +98,12 @@ namespace Ryujinx.Graphics.Gpu.Image
case Format.Bc1RgbaSrgb:
case Format.Bc2Srgb:
case Format.Bc3Srgb:
case Format.Bc7Srgb:
return new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4);
case Format.Bc1RgbaUnorm:
case Format.Bc2Unorm:
case Format.Bc3Unorm:
case Format.Bc7Unorm:
return new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4);
case Format.Bc4Unorm:
return new FormatInfo(Format.R8Unorm, 1, 1, 1, 1);
@ -107,12 +113,50 @@ namespace Ryujinx.Graphics.Gpu.Image
return new FormatInfo(Format.R8G8Unorm, 1, 1, 2, 2);
case Format.Bc5Snorm:
return new FormatInfo(Format.R8G8Snorm, 1, 1, 2, 2);
case Format.Bc6HSfloat:
case Format.Bc6HUfloat:
return new FormatInfo(Format.R16G16B16A16Float, 1, 1, 8, 4);
}
}
return info.FormatInfo;
}
/// <summary>
/// Checks if the host API supports a given texture compression format of the BC family.
/// </summary>
/// <param name="format">BC format to be checked</param>
/// <param name="target">Target usage of the texture</param>
/// <param name="caps">Host GPU Capabilities</param>
/// <returns>True if the texture host supports the format with the given target usage, false otherwise</returns>
public static bool HostSupportsBcFormat(Format format, Target target, Capabilities caps)
{
bool not3DOr3DCompressionSupported = target != Target.Texture3D || caps.Supports3DTextureCompression;
switch (format)
{
case Format.Bc1RgbaSrgb:
case Format.Bc1RgbaUnorm:
case Format.Bc2Srgb:
case Format.Bc2Unorm:
case Format.Bc3Srgb:
case Format.Bc3Unorm:
return caps.SupportsBc123Compression && not3DOr3DCompressionSupported;
case Format.Bc4Unorm:
case Format.Bc4Snorm:
case Format.Bc5Unorm:
case Format.Bc5Snorm:
return caps.SupportsBc45Compression && not3DOr3DCompressionSupported;
case Format.Bc6HSfloat:
case Format.Bc6HUfloat:
case Format.Bc7Srgb:
case Format.Bc7Unorm:
return caps.SupportsBc67Compression && not3DOr3DCompressionSupported;
}
return true;
}
/// <summary>
/// Determines whether a texture can flush its data back to guest memory.
/// </summary>
@ -627,9 +671,9 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
/// <param name="lhs">Texture information of the texture view</param
/// <param name="rhs">Texture information of the texture view</param>
/// <param name="isCopy">True to check for copy rather than view compatibility</param>
/// <param name="caps">Host GPU capabilities</param>
/// <returns>True if the targets are compatible, false otherwise</returns>
public static TextureViewCompatibility ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs)
public static TextureViewCompatibility ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs, ref Capabilities caps)
{
bool result = false;
switch (lhs.Target)
@ -646,14 +690,24 @@ namespace Ryujinx.Graphics.Gpu.Image
break;
case Target.Texture2DArray:
result = rhs.Target == Target.Texture2D ||
rhs.Target == Target.Texture2DArray;
if (rhs.Target == Target.Cubemap || rhs.Target == Target.CubemapArray)
{
return caps.SupportsCubemapView ? TextureViewCompatibility.Full : TextureViewCompatibility.CopyOnly;
}
break;
case Target.Cubemap:
case Target.CubemapArray:
result = rhs.Target == Target.Texture2D ||
rhs.Target == Target.Texture2DArray ||
rhs.Target == Target.Cubemap ||
result = rhs.Target == Target.Cubemap ||
rhs.Target == Target.CubemapArray;
break;
if (rhs.Target == Target.Texture2D || rhs.Target == Target.Texture2DArray)
{
return caps.SupportsCubemapView ? TextureViewCompatibility.Full : TextureViewCompatibility.CopyOnly;
}
break;
case Target.Texture2DMultisample:
case Target.Texture2DMultisampleArray:
if (rhs.Target == Target.Texture2D || rhs.Target == Target.Texture2DArray)

View file

@ -1,4 +1,3 @@
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;

View file

@ -435,7 +435,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
else
{
_context.Renderer.Pipeline.SetTexture(binding.BindingInfo.Binding, binding.Texture);
_context.Renderer.Pipeline.SetTextureAndSampler(binding.Stage, binding.BindingInfo.Binding, binding.Texture, null);
}
}
@ -719,17 +719,25 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary>
/// Sets the buffer storage of a buffer texture. This will be bound when the buffer manager commits bindings.
/// </summary>
/// <param name="stage">Shader stage accessing the texture</param>
/// <param name="texture">Buffer texture</param>
/// <param name="address">Address of the buffer in memory</param>
/// <param name="size">Size of the buffer in bytes</param>
/// <param name="bindingInfo">Binding info for the buffer texture</param>
/// <param name="format">Format of the buffer texture</param>
/// <param name="isImage">Whether the binding is for an image or a sampler</param>
public void SetBufferTextureStorage(ITexture texture, ulong address, ulong size, TextureBindingInfo bindingInfo, Format format, bool isImage)
public void SetBufferTextureStorage(
ShaderStage stage,
ITexture texture,
ulong address,
ulong size,
TextureBindingInfo bindingInfo,
Format format,
bool isImage)
{
_channel.MemoryManager.Physical.BufferCache.CreateBuffer(address, size);
_bufferTextures.Add(new BufferTextureBinding(texture, address, size, bindingInfo, format, isImage));
_bufferTextures.Add(new BufferTextureBinding(stage, texture, address, size, bindingInfo, format, isImage));
}
/// <summary>

View file

@ -1,5 +1,6 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Shader;
namespace Ryujinx.Graphics.Gpu.Memory
{
@ -8,6 +9,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// </summary>
struct BufferTextureBinding
{
/// <summary>
/// Shader stage accessing the texture.
/// </summary>
public ShaderStage Stage { get; }
/// <summary>
/// The buffer texture.
/// </summary>
@ -41,14 +47,23 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// <summary>
/// Create a new buffer texture binding.
/// </summary>
/// <param name="stage">Shader stage accessing the texture</param>
/// <param name="texture">Buffer texture</param>
/// <param name="address">Base address</param>
/// <param name="size">Size in bytes</param>
/// <param name="bindingInfo">Binding info</param>
/// <param name="format">Binding format</param>
/// <param name="isImage">Whether the binding is for an image or a sampler</param>
public BufferTextureBinding(ITexture texture, ulong address, ulong size, TextureBindingInfo bindingInfo, Format format, bool isImage)
public BufferTextureBinding(
ShaderStage stage,
ITexture texture,
ulong address,
ulong size,
TextureBindingInfo bindingInfo,
Format format,
bool isImage)
{
Stage = stage;
Texture = texture;
Address = address;
Size = size;

View file

@ -14,8 +14,4 @@
<ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SharpZipLib" Version="1.3.3" />
</ItemGroup>
</Project>

View file

@ -1,617 +0,0 @@
using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
/// <summary>
/// Represent a cache collection handling one shader cache.
/// </summary>
class CacheCollection : IDisposable
{
/// <summary>
/// Possible operation to do on the <see cref="_fileWriterWorkerQueue"/>.
/// </summary>
private enum CacheFileOperation
{
/// <summary>
/// Save a new entry in the temp cache.
/// </summary>
SaveTempEntry,
/// <summary>
/// Save the hash manifest.
/// </summary>
SaveManifest,
/// <summary>
/// Remove entries from the hash manifest and save it.
/// </summary>
RemoveManifestEntries,
/// <summary>
/// Remove entries from the hash manifest and save it, and also deletes the temporary file.
/// </summary>
RemoveManifestEntryAndTempFile,
/// <summary>
/// Flush temporary cache to archive.
/// </summary>
FlushToArchive,
/// <summary>
/// Signal when hitting this point. This is useful to know if all previous operations were performed.
/// </summary>
Synchronize
}
/// <summary>
/// Represent an operation to perform on the <see cref="_fileWriterWorkerQueue"/>.
/// </summary>
private class CacheFileOperationTask
{
/// <summary>
/// The type of operation to perform.
/// </summary>
public CacheFileOperation Type;
/// <summary>
/// The data associated to this operation or null.
/// </summary>
public object Data;
}
/// <summary>
/// Data associated to the <see cref="CacheFileOperation.SaveTempEntry"/> operation.
/// </summary>
private class CacheFileSaveEntryTaskData
{
/// <summary>
/// The key of the entry to cache.
/// </summary>
public Hash128 Key;
/// <summary>
/// The value of the entry to cache.
/// </summary>
public byte[] Value;
}
/// <summary>
/// The directory of the shader cache.
/// </summary>
private readonly string _cacheDirectory;
/// <summary>
/// The version of the cache.
/// </summary>
private readonly ulong _version;
/// <summary>
/// The hash type of the cache.
/// </summary>
private readonly CacheHashType _hashType;
/// <summary>
/// The graphics API of the cache.
/// </summary>
private readonly CacheGraphicsApi _graphicsApi;
/// <summary>
/// The table of all the hash registered in the cache.
/// </summary>
private HashSet<Hash128> _hashTable;
/// <summary>
/// The queue of operations to be performed by the file writer worker.
/// </summary>
private AsyncWorkQueue<CacheFileOperationTask> _fileWriterWorkerQueue;
/// <summary>
/// Main storage of the cache collection.
/// </summary>
private ZipFile _cacheArchive;
/// <summary>
/// Indicates if the cache collection supports modification.
/// </summary>
public bool IsReadOnly { get; }
/// <summary>
/// Immutable copy of the hash table.
/// </summary>
public ReadOnlySpan<Hash128> HashTable => _hashTable.ToArray();
/// <summary>
/// Get the temp path to the cache data directory.
/// </summary>
/// <returns>The temp path to the cache data directory</returns>
private string GetCacheTempDataPath() => CacheHelper.GetCacheTempDataPath(_cacheDirectory);
/// <summary>
/// The path to the cache archive file.
/// </summary>
/// <returns>The path to the cache archive file</returns>
private string GetArchivePath() => CacheHelper.GetArchivePath(_cacheDirectory);
/// <summary>
/// The path to the cache manifest file.
/// </summary>
/// <returns>The path to the cache manifest file</returns>
private string GetManifestPath() => CacheHelper.GetManifestPath(_cacheDirectory);
/// <summary>
/// Create a new temp path to the given cached file via its hash.
/// </summary>
/// <param name="key">The hash of the cached data</param>
/// <returns>New path to the given cached file</returns>
private string GenCacheTempFilePath(Hash128 key) => CacheHelper.GenCacheTempFilePath(_cacheDirectory, key);
/// <summary>
/// Create a new cache collection.
/// </summary>
/// <param name="baseCacheDirectory">The directory of the shader cache</param>
/// <param name="hashType">The hash type of the shader cache</param>
/// <param name="graphicsApi">The graphics api of the shader cache</param>
/// <param name="shaderProvider">The shader provider name of the shader cache</param>
/// <param name="cacheName">The name of the cache</param>
/// <param name="version">The version of the cache</param>
public CacheCollection(string baseCacheDirectory, CacheHashType hashType, CacheGraphicsApi graphicsApi, string shaderProvider, string cacheName, ulong version)
{
if (hashType != CacheHashType.XxHash128)
{
throw new NotImplementedException($"{hashType}");
}
_cacheDirectory = CacheHelper.GenerateCachePath(baseCacheDirectory, graphicsApi, shaderProvider, cacheName);
_graphicsApi = graphicsApi;
_hashType = hashType;
_version = version;
_hashTable = new HashSet<Hash128>();
IsReadOnly = CacheHelper.IsArchiveReadOnly(GetArchivePath());
Load();
_fileWriterWorkerQueue = new AsyncWorkQueue<CacheFileOperationTask>(HandleCacheTask, $"CacheCollection.Worker.{cacheName}");
}
/// <summary>
/// Load the cache manifest file and recreate it if invalid.
/// </summary>
private void Load()
{
bool isValid = false;
if (Directory.Exists(_cacheDirectory))
{
string manifestPath = GetManifestPath();
if (File.Exists(manifestPath))
{
Memory<byte> rawManifest = File.ReadAllBytes(manifestPath);
if (MemoryMarshal.TryRead(rawManifest.Span, out CacheManifestHeader manifestHeader))
{
Memory<byte> hashTableRaw = rawManifest.Slice(Unsafe.SizeOf<CacheManifestHeader>());
isValid = manifestHeader.IsValid(_graphicsApi, _hashType, hashTableRaw.Span) && _version == manifestHeader.Version;
if (isValid)
{
ReadOnlySpan<Hash128> hashTable = MemoryMarshal.Cast<byte, Hash128>(hashTableRaw.Span);
foreach (Hash128 hash in hashTable)
{
_hashTable.Add(hash);
}
}
}
}
}
if (!isValid)
{
Logger.Warning?.Print(LogClass.Gpu, $"Shader collection \"{_cacheDirectory}\" got invalidated, cache will need to be rebuilt.");
if (Directory.Exists(_cacheDirectory))
{
Directory.Delete(_cacheDirectory, true);
}
Directory.CreateDirectory(_cacheDirectory);
SaveManifest();
}
FlushToArchive();
}
/// <summary>
/// Queue a task to remove entries from the hash manifest.
/// </summary>
/// <param name="entries">Entries to remove from the manifest</param>
public void RemoveManifestEntriesAsync(HashSet<Hash128> entries)
{
if (IsReadOnly)
{
Logger.Warning?.Print(LogClass.Gpu, "Trying to remove manifest entries on a read-only cache, ignoring.");
return;
}
_fileWriterWorkerQueue.Add(new CacheFileOperationTask
{
Type = CacheFileOperation.RemoveManifestEntries,
Data = entries
});
}
/// <summary>
/// Remove given entries from the manifest.
/// </summary>
/// <param name="entries">Entries to remove from the manifest</param>
private void RemoveManifestEntries(HashSet<Hash128> entries)
{
lock (_hashTable)
{
foreach (Hash128 entry in entries)
{
_hashTable.Remove(entry);
}
SaveManifest();
}
}
/// <summary>
/// Remove given entry from the manifest and delete the temporary file.
/// </summary>
/// <param name="entry">Entry to remove from the manifest</param>
private void RemoveManifestEntryAndTempFile(Hash128 entry)
{
lock (_hashTable)
{
_hashTable.Remove(entry);
SaveManifest();
}
File.Delete(GenCacheTempFilePath(entry));
}
/// <summary>
/// Queue a task to flush temporary files to the archive on the worker.
/// </summary>
public void FlushToArchiveAsync()
{
_fileWriterWorkerQueue.Add(new CacheFileOperationTask
{
Type = CacheFileOperation.FlushToArchive
});
}
/// <summary>
/// Wait for all tasks before this given point to be done.
/// </summary>
public void Synchronize()
{
using (ManualResetEvent evnt = new ManualResetEvent(false))
{
_fileWriterWorkerQueue.Add(new CacheFileOperationTask
{
Type = CacheFileOperation.Synchronize,
Data = evnt
});
evnt.WaitOne();
}
}
/// <summary>
/// Flush temporary files to the archive.
/// </summary>
/// <remarks>This dispose <see cref="_cacheArchive"/> if not null and reinstantiate it.</remarks>
private void FlushToArchive()
{
EnsureArchiveUpToDate();
// Open the zip in readonly to avoid anyone modifying/corrupting it during normal operations.
_cacheArchive = new ZipFile(File.OpenRead(GetArchivePath()));
}
/// <summary>
/// Save temporary files not in archive.
/// </summary>
/// <remarks>This dispose <see cref="_cacheArchive"/> if not null.</remarks>
public void EnsureArchiveUpToDate()
{
// First close previous opened instance if found.
if (_cacheArchive != null)
{
_cacheArchive.Close();
}
string archivePath = GetArchivePath();
if (IsReadOnly)
{
Logger.Warning?.Print(LogClass.Gpu, $"Cache collection archive in read-only, archiving task skipped.");
return;
}
if (CacheHelper.IsArchiveReadOnly(archivePath))
{
Logger.Warning?.Print(LogClass.Gpu, $"Cache collection archive in use, archiving task skipped.");
return;
}
if (!File.Exists(archivePath))
{
using (ZipFile newZip = ZipFile.Create(archivePath))
{
// Workaround for SharpZipLib issue #395
newZip.BeginUpdate();
newZip.CommitUpdate();
}
}
// Open the zip in read/write.
_cacheArchive = new ZipFile(File.Open(archivePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None));
Logger.Info?.Print(LogClass.Gpu, $"Updating cache collection archive {archivePath}...");
// Update the content of the zip.
lock (_hashTable)
{
CacheHelper.EnsureArchiveUpToDate(_cacheDirectory, _cacheArchive, _hashTable);
// Close the instance to force a flush.
_cacheArchive.Close();
_cacheArchive = null;
string cacheTempDataPath = GetCacheTempDataPath();
// Create the cache data path if missing.
if (!Directory.Exists(cacheTempDataPath))
{
Directory.CreateDirectory(cacheTempDataPath);
}
}
Logger.Info?.Print(LogClass.Gpu, $"Updated cache collection archive {archivePath}.");
}
/// <summary>
/// Save the manifest file.
/// </summary>
private void SaveManifest()
{
byte[] data;
lock (_hashTable)
{
data = CacheHelper.ComputeManifest(_version, _graphicsApi, _hashType, _hashTable);
}
File.WriteAllBytes(GetManifestPath(), data);
}
/// <summary>
/// Get a cached file with the given hash.
/// </summary>
/// <param name="keyHash">The given hash</param>
/// <returns>The cached file if present or null</returns>
public byte[] GetValueRaw(ref Hash128 keyHash)
{
return GetValueRawFromArchive(ref keyHash) ?? GetValueRawFromFile(ref keyHash);
}
/// <summary>
/// Get a cached file with the given hash that is present in the archive.
/// </summary>
/// <param name="keyHash">The given hash</param>
/// <returns>The cached file if present or null</returns>
private byte[] GetValueRawFromArchive(ref Hash128 keyHash)
{
bool found;
lock (_hashTable)
{
found = _hashTable.Contains(keyHash);
}
if (found)
{
return CacheHelper.ReadFromArchive(_cacheArchive, keyHash);
}
return null;
}
/// <summary>
/// Get a cached file with the given hash that is not present in the archive.
/// </summary>
/// <param name="keyHash">The given hash</param>
/// <returns>The cached file if present or null</returns>
private byte[] GetValueRawFromFile(ref Hash128 keyHash)
{
bool found;
lock (_hashTable)
{
found = _hashTable.Contains(keyHash);
}
if (found)
{
return CacheHelper.ReadFromFile(GetCacheTempDataPath(), keyHash);
}
return null;
}
private void HandleCacheTask(CacheFileOperationTask task)
{
switch (task.Type)
{
case CacheFileOperation.SaveTempEntry:
SaveTempEntry((CacheFileSaveEntryTaskData)task.Data);
break;
case CacheFileOperation.SaveManifest:
SaveManifest();
break;
case CacheFileOperation.RemoveManifestEntries:
RemoveManifestEntries((HashSet<Hash128>)task.Data);
break;
case CacheFileOperation.RemoveManifestEntryAndTempFile:
RemoveManifestEntryAndTempFile((Hash128)task.Data);
break;
case CacheFileOperation.FlushToArchive:
FlushToArchive();
break;
case CacheFileOperation.Synchronize:
((ManualResetEvent)task.Data).Set();
break;
default:
throw new NotImplementedException($"{task.Type}");
}
}
/// <summary>
/// Save a new entry in the temp cache.
/// </summary>
/// <param name="entry">The entry to save in the temp cache</param>
private void SaveTempEntry(CacheFileSaveEntryTaskData entry)
{
string tempPath = GenCacheTempFilePath(entry.Key);
File.WriteAllBytes(tempPath, entry.Value);
}
/// <summary>
/// Add a new value in the cache with a given hash.
/// </summary>
/// <param name="keyHash">The hash to use for the value in the cache</param>
/// <param name="value">The value to cache</param>
public void AddValue(ref Hash128 keyHash, byte[] value)
{
if (IsReadOnly)
{
Logger.Warning?.Print(LogClass.Gpu, $"Trying to add {keyHash} on a read-only cache, ignoring.");
return;
}
Debug.Assert(value != null);
bool isAlreadyPresent;
lock (_hashTable)
{
isAlreadyPresent = !_hashTable.Add(keyHash);
}
if (isAlreadyPresent)
{
// NOTE: Used for debug
File.WriteAllBytes(GenCacheTempFilePath(new Hash128()), value);
throw new InvalidOperationException($"Cache collision found on {GenCacheTempFilePath(keyHash)}");
}
// Queue file change operations
_fileWriterWorkerQueue.Add(new CacheFileOperationTask
{
Type = CacheFileOperation.SaveTempEntry,
Data = new CacheFileSaveEntryTaskData
{
Key = keyHash,
Value = value
}
});
// Save the manifest changes
_fileWriterWorkerQueue.Add(new CacheFileOperationTask
{
Type = CacheFileOperation.SaveManifest,
});
}
/// <summary>
/// Replace a value at the given hash in the cache.
/// </summary>
/// <param name="keyHash">The hash to use for the value in the cache</param>
/// <param name="value">The value to cache</param>
public void ReplaceValue(ref Hash128 keyHash, byte[] value)
{
if (IsReadOnly)
{
Logger.Warning?.Print(LogClass.Gpu, $"Trying to replace {keyHash} on a read-only cache, ignoring.");
return;
}
Debug.Assert(value != null);
// Only queue file change operations
_fileWriterWorkerQueue.Add(new CacheFileOperationTask
{
Type = CacheFileOperation.SaveTempEntry,
Data = new CacheFileSaveEntryTaskData
{
Key = keyHash,
Value = value
}
});
}
/// <summary>
/// Removes a value at the given hash from the cache.
/// </summary>
/// <param name="keyHash">The hash of the value in the cache</param>
public void RemoveValue(ref Hash128 keyHash)
{
if (IsReadOnly)
{
Logger.Warning?.Print(LogClass.Gpu, $"Trying to remove {keyHash} on a read-only cache, ignoring.");
return;
}
// Only queue file change operations
_fileWriterWorkerQueue.Add(new CacheFileOperationTask
{
Type = CacheFileOperation.RemoveManifestEntryAndTempFile,
Data = keyHash
});
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Make sure all operations on _fileWriterWorkerQueue are done.
Synchronize();
_fileWriterWorkerQueue.Dispose();
EnsureArchiveUpToDate();
}
}
}
}

View file

@ -1,273 +0,0 @@
using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using Ryujinx.Graphics.Shader;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
/// <summary>
/// Helper to manipulate the disk shader cache.
/// </summary>
static class CacheHelper
{
/// <summary>
/// Compute a cache manifest from runtime data.
/// </summary>
/// <param name="version">The version of the cache</param>
/// <param name="graphicsApi">The graphics api used by the cache</param>
/// <param name="hashType">The hash type of the cache</param>
/// <param name="entries">The entries in the cache</param>
/// <returns>The cache manifest from runtime data</returns>
public static byte[] ComputeManifest(ulong version, CacheGraphicsApi graphicsApi, CacheHashType hashType, HashSet<Hash128> entries)
{
if (hashType != CacheHashType.XxHash128)
{
throw new NotImplementedException($"{hashType}");
}
CacheManifestHeader manifestHeader = new CacheManifestHeader(version, graphicsApi, hashType);
byte[] data = new byte[Unsafe.SizeOf<CacheManifestHeader>() + entries.Count * Unsafe.SizeOf<Hash128>()];
// CacheManifestHeader has the same size as a Hash128.
Span<Hash128> dataSpan = MemoryMarshal.Cast<byte, Hash128>(data.AsSpan()).Slice(1);
int i = 0;
foreach (Hash128 hash in entries)
{
dataSpan[i++] = hash;
}
manifestHeader.UpdateChecksum(data.AsSpan(Unsafe.SizeOf<CacheManifestHeader>()));
MemoryMarshal.Write(data, ref manifestHeader);
return data;
}
/// <summary>
/// Get the base directory of the shader cache for a given title id.
/// </summary>
/// <param name="titleId">The title id of the target application</param>
/// <returns>The base directory of the shader cache for a given title id</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetBaseCacheDirectory(string titleId) => Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "shader");
/// <summary>
/// Get the temp path to the cache data directory.
/// </summary>
/// <param name="cacheDirectory">The cache directory</param>
/// <returns>The temp path to the cache data directory</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetCacheTempDataPath(string cacheDirectory) => Path.Combine(cacheDirectory, "temp");
/// <summary>
/// The path to the cache archive file.
/// </summary>
/// <param name="cacheDirectory">The cache directory</param>
/// <returns>The path to the cache archive file</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetArchivePath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.zip");
/// <summary>
/// The path to the cache manifest file.
/// </summary>
/// <param name="cacheDirectory">The cache directory</param>
/// <returns>The path to the cache manifest file</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetManifestPath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.info");
/// <summary>
/// Create a new temp path to the given cached file via its hash.
/// </summary>
/// <param name="cacheDirectory">The cache directory</param>
/// <param name="key">The hash of the cached data</param>
/// <returns>New path to the given cached file</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GenCacheTempFilePath(string cacheDirectory, Hash128 key) => Path.Combine(GetCacheTempDataPath(cacheDirectory), key.ToString());
/// <summary>
/// Generate the path to the cache directory.
/// </summary>
/// <param name="baseCacheDirectory">The base of the cache directory</param>
/// <param name="graphicsApi">The graphics api in use</param>
/// <param name="shaderProvider">The name of the shader provider in use</param>
/// <param name="cacheName">The name of the cache</param>
/// <returns>The path to the cache directory</returns>
public static string GenerateCachePath(string baseCacheDirectory, CacheGraphicsApi graphicsApi, string shaderProvider, string cacheName)
{
string graphicsApiName = graphicsApi switch
{
CacheGraphicsApi.OpenGL => "opengl",
CacheGraphicsApi.OpenGLES => "opengles",
CacheGraphicsApi.Vulkan => "vulkan",
CacheGraphicsApi.DirectX => "directx",
CacheGraphicsApi.Metal => "metal",
CacheGraphicsApi.Guest => "guest",
_ => throw new NotImplementedException(graphicsApi.ToString()),
};
return Path.Combine(baseCacheDirectory, graphicsApiName, shaderProvider, cacheName);
}
/// <summary>
/// Read a cached file with the given hash that is present in the archive.
/// </summary>
/// <param name="archive">The archive in use</param>
/// <param name="entry">The given hash</param>
/// <returns>The cached file if present or null</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadFromArchive(ZipFile archive, Hash128 entry)
{
if (archive != null)
{
ZipEntry archiveEntry = archive.GetEntry($"{entry}");
if (archiveEntry != null)
{
try
{
byte[] result = new byte[archiveEntry.Size];
using (Stream archiveStream = archive.GetInputStream(archiveEntry))
{
archiveStream.Read(result);
return result;
}
}
catch (Exception e)
{
Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file {entry} from archive");
Logger.Error?.Print(LogClass.Gpu, e.ToString());
}
}
}
return null;
}
/// <summary>
/// Read a cached file with the given hash that is not present in the archive.
/// </summary>
/// <param name="cacheDirectory">The cache directory</param>
/// <param name="entry">The given hash</param>
/// <returns>The cached file if present or null</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] ReadFromFile(string cacheDirectory, Hash128 entry)
{
string cacheTempFilePath = GenCacheTempFilePath(cacheDirectory, entry);
try
{
return File.ReadAllBytes(cacheTempFilePath);
}
catch (Exception e)
{
Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file at {cacheTempFilePath}");
Logger.Error?.Print(LogClass.Gpu, e.ToString());
}
return null;
}
/// <summary>
/// Read transform feedback descriptors from guest.
/// </summary>
/// <param name="data">The raw guest transform feedback descriptors</param>
/// <param name="header">The guest shader program header</param>
/// <returns>The transform feedback descriptors read from guest</returns>
public static TransformFeedbackDescriptorOld[] ReadTransformFeedbackInformation(ref ReadOnlySpan<byte> data, GuestShaderCacheHeader header)
{
if (header.TransformFeedbackCount != 0)
{
TransformFeedbackDescriptorOld[] result = new TransformFeedbackDescriptorOld[header.TransformFeedbackCount];
for (int i = 0; i < result.Length; i++)
{
GuestShaderCacheTransformFeedbackHeader feedbackHeader = MemoryMarshal.Read<GuestShaderCacheTransformFeedbackHeader>(data);
result[i] = new TransformFeedbackDescriptorOld(feedbackHeader.BufferIndex, feedbackHeader.Stride, data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>(), feedbackHeader.VaryingLocationsLength).ToArray());
data = data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>() + feedbackHeader.VaryingLocationsLength);
}
return result;
}
return null;
}
/// <summary>
/// Save temporary files not in archive.
/// </summary>
/// <param name="baseCacheDirectory">The base of the cache directory</param>
/// <param name="archive">The archive to use</param>
/// <param name="entries">The entries in the cache</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipFile archive, HashSet<Hash128> entries)
{
List<string> filesToDelete = new List<string>();
archive.BeginUpdate();
foreach (Hash128 hash in entries)
{
string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash);
if (File.Exists(cacheTempFilePath))
{
string cacheHash = $"{hash}";
ZipEntry entry = archive.GetEntry(cacheHash);
if (entry != null)
{
archive.Delete(entry);
}
// We enforce deflate compression here to avoid possible incompatibilities on older version of Ryujinx that use System.IO.Compression.
archive.Add(new StaticDiskDataSource(cacheTempFilePath), cacheHash, CompressionMethod.Deflated);
filesToDelete.Add(cacheTempFilePath);
}
}
archive.CommitUpdate();
foreach (string filePath in filesToDelete)
{
File.Delete(filePath);
}
}
public static bool IsArchiveReadOnly(string archivePath)
{
FileInfo info = new FileInfo(archivePath);
if (!info.Exists)
{
return false;
}
try
{
using (FileStream stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.None))
{
return false;
}
}
catch (IOException)
{
return true;
}
}
}
}

View file

@ -1,168 +0,0 @@
using Ryujinx.Common;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using System;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
/// <summary>
/// Global Manager of the shader cache.
/// </summary>
class CacheManager : IDisposable
{
private CacheGraphicsApi _graphicsApi;
private CacheHashType _hashType;
private string _shaderProvider;
/// <summary>
/// Cache storing raw Maxwell shaders as programs.
/// </summary>
private CacheCollection _guestProgramCache;
/// <summary>
/// Cache storing raw host programs.
/// </summary>
private CacheCollection _hostProgramCache;
/// <summary>
/// Version of the guest cache shader (to increment when guest cache structure change).
/// </summary>
private const ulong GuestCacheVersion = 1759;
public bool IsReadOnly => _guestProgramCache.IsReadOnly || _hostProgramCache.IsReadOnly;
/// <summary>
/// Create a new cache manager instance
/// </summary>
/// <param name="graphicsApi">The graphics api in use</param>
/// <param name="hashType">The hash type in use for the cache</param>
/// <param name="shaderProvider">The name of the codegen provider</param>
/// <param name="titleId">The guest application title ID</param>
/// <param name="shaderCodeGenVersion">Version of the codegen</param>
public CacheManager(CacheGraphicsApi graphicsApi, CacheHashType hashType, string shaderProvider, string titleId, ulong shaderCodeGenVersion)
{
_graphicsApi = graphicsApi;
_hashType = hashType;
_shaderProvider = shaderProvider;
string baseCacheDirectory = CacheHelper.GetBaseCacheDirectory(titleId);
_guestProgramCache = new CacheCollection(baseCacheDirectory, _hashType, CacheGraphicsApi.Guest, "", "program", GuestCacheVersion);
_hostProgramCache = new CacheCollection(baseCacheDirectory, _hashType, _graphicsApi, _shaderProvider, "host", shaderCodeGenVersion);
}
/// <summary>
/// Entries to remove from the manifest.
/// </summary>
/// <param name="entries">Entries to remove from the manifest of all caches</param>
public void RemoveManifestEntries(HashSet<Hash128> entries)
{
_guestProgramCache.RemoveManifestEntriesAsync(entries);
_hostProgramCache.RemoveManifestEntriesAsync(entries);
}
/// <summary>
/// Queue a task to flush temporary files to the archives.
/// </summary>
public void FlushToArchive()
{
_guestProgramCache.FlushToArchiveAsync();
_hostProgramCache.FlushToArchiveAsync();
}
/// <summary>
/// Wait for all tasks before this given point to be done.
/// </summary>
public void Synchronize()
{
_guestProgramCache.Synchronize();
_hostProgramCache.Synchronize();
}
/// <summary>
/// Save a shader program not present in the program cache.
/// </summary>
/// <param name="programCodeHash">Target program code hash</param>
/// <param name="guestProgram">Guest program raw data</param>
/// <param name="hostProgram">Host program raw data</param>
public void SaveProgram(ref Hash128 programCodeHash, byte[] guestProgram, byte[] hostProgram)
{
_guestProgramCache.AddValue(ref programCodeHash, guestProgram);
_hostProgramCache.AddValue(ref programCodeHash, hostProgram);
}
/// <summary>
/// Add a host shader program not present in the program cache.
/// </summary>
/// <param name="programCodeHash">Target program code hash</param>
/// <param name="data">Host program raw data</param>
public void AddHostProgram(ref Hash128 programCodeHash, byte[] data)
{
_hostProgramCache.AddValue(ref programCodeHash, data);
}
/// <summary>
/// Replace a host shader program present in the program cache.
/// </summary>
/// <param name="programCodeHash">Target program code hash</param>
/// <param name="data">Host program raw data</param>
public void ReplaceHostProgram(ref Hash128 programCodeHash, byte[] data)
{
_hostProgramCache.ReplaceValue(ref programCodeHash, data);
}
/// <summary>
/// Removes a shader program present in the program cache.
/// </summary>
/// <param name="programCodeHash">Target program code hash</param>
public void RemoveProgram(ref Hash128 programCodeHash)
{
_guestProgramCache.RemoveValue(ref programCodeHash);
_hostProgramCache.RemoveValue(ref programCodeHash);
}
/// <summary>
/// Get all guest program hashes.
/// </summary>
/// <returns>All guest program hashes</returns>
public ReadOnlySpan<Hash128> GetGuestProgramList()
{
return _guestProgramCache.HashTable;
}
/// <summary>
/// Get a host program by hash.
/// </summary>
/// <param name="hash">The given hash</param>
/// <returns>The host program if present or null</returns>
public byte[] GetHostProgramByHash(ref Hash128 hash)
{
return _hostProgramCache.GetValueRaw(ref hash);
}
/// <summary>
/// Get a guest program by hash.
/// </summary>
/// <param name="hash">The given hash</param>
/// <returns>The guest program if present or null</returns>
public byte[] GetGuestProgramByHash(ref Hash128 hash)
{
return _guestProgramCache.GetValueRaw(ref hash);
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_guestProgramCache.Dispose();
_hostProgramCache.Dispose();
}
}
}
}

View file

@ -1,38 +0,0 @@
namespace Ryujinx.Graphics.Gpu.Shader.Cache.Definition
{
/// <summary>
/// Graphics API type accepted by the shader cache.
/// </summary>
enum CacheGraphicsApi : byte
{
/// <summary>
/// OpenGL Core
/// </summary>
OpenGL,
/// <summary>
/// OpenGL ES
/// </summary>
OpenGLES,
/// <summary>
/// Vulkan
/// </summary>
Vulkan,
/// <summary>
/// DirectX
/// </summary>
DirectX,
/// <summary>
/// Metal
/// </summary>
Metal,
/// <summary>
/// Guest, used to cache games raw shader programs.
/// </summary>
Guest
}
}

View file

@ -1,13 +0,0 @@
namespace Ryujinx.Graphics.Gpu.Shader.Cache.Definition
{
/// <summary>
/// Hash algorithm accepted by the shader cache.
/// </summary>
enum CacheHashType : byte
{
/// <summary>
/// xxHash128
/// </summary>
XxHash128
}
}

View file

@ -1,97 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader.Cache.Definition
{
/// <summary>
/// Header of the shader cache manifest.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)]
struct CacheManifestHeader
{
/// <summary>
/// The version of the cache.
/// </summary>
public ulong Version;
/// <summary>
/// The graphics api used for this cache.
/// </summary>
public CacheGraphicsApi GraphicsApi;
/// <summary>
/// The hash type used for this cache.
/// </summary>
public CacheHashType HashType;
/// <summary>
/// CRC-16 checksum over the data in the file.
/// </summary>
public ushort TableChecksum;
/// <summary>
/// Construct a new cache manifest header.
/// </summary>
/// <param name="version">The version of the cache</param>
/// <param name="graphicsApi">The graphics api used for this cache</param>
/// <param name="hashType">The hash type used for this cache</param>
public CacheManifestHeader(ulong version, CacheGraphicsApi graphicsApi, CacheHashType hashType)
{
Version = version;
GraphicsApi = graphicsApi;
HashType = hashType;
TableChecksum = 0;
}
/// <summary>
/// Update the checksum in the header.
/// </summary>
/// <param name="data">The data to perform the checksum on</param>
public void UpdateChecksum(ReadOnlySpan<byte> data)
{
TableChecksum = CalculateCrc16(data);
}
/// <summary>
/// Calculate a CRC-16 over data.
/// </summary>
/// <param name="data">The data to perform the CRC-16 on</param>
/// <returns>A CRC-16 over data</returns>
private static ushort CalculateCrc16(ReadOnlySpan<byte> data)
{
int crc = 0;
const ushort poly = 0x1021;
for (int i = 0; i < data.Length; i++)
{
crc ^= data[i] << 8;
for (int j = 0; j < 8; j++)
{
crc <<= 1;
if ((crc & 0x10000) != 0)
{
crc = (crc ^ poly) & 0xFFFF;
}
}
}
return (ushort)crc;
}
/// <summary>
/// Check the validity of the header.
/// </summary>
/// <param name="graphicsApi">The target graphics api in use</param>
/// <param name="hashType">The target hash type in use</param>
/// <param name="data">The data after this header</param>
/// <returns>True if the header is valid</returns>
/// <remarks>This doesn't check that versions match</remarks>
public bool IsValid(CacheGraphicsApi graphicsApi, CacheHashType hashType, ReadOnlySpan<byte> data)
{
return GraphicsApi == graphicsApi && HashType == hashType && TableChecksum == CalculateCrc16(data);
}
}
}

View file

@ -1,67 +0,0 @@
using Ryujinx.Graphics.Shader;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader.Cache.Definition
{
/// <summary>
/// Header of a cached guest gpu accessor.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)]
struct GuestGpuAccessorHeader
{
/// <summary>
/// The count of texture descriptors.
/// </summary>
public int TextureDescriptorCount;
/// <summary>
/// Local Size X for compute shaders.
/// </summary>
public int ComputeLocalSizeX;
/// <summary>
/// Local Size Y for compute shaders.
/// </summary>
public int ComputeLocalSizeY;
/// <summary>
/// Local Size Z for compute shaders.
/// </summary>
public int ComputeLocalSizeZ;
/// <summary>
/// Local Memory size in bytes for compute shaders.
/// </summary>
public int ComputeLocalMemorySize;
/// <summary>
/// Shared Memory size in bytes for compute shaders.
/// </summary>
public int ComputeSharedMemorySize;
/// <summary>
/// Unused/reserved.
/// </summary>
public int Reserved1;
/// <summary>
/// Current primitive topology for geometry shaders.
/// </summary>
public InputTopology PrimitiveTopology;
/// <summary>
/// Tessellation parameters (packed to fit on a byte).
/// </summary>
public byte TessellationModePacked;
/// <summary>
/// Unused/reserved.
/// </summary>
public byte Reserved2;
/// <summary>
/// GPU boolean state that can influence shader compilation.
/// </summary>
public GuestGpuStateFlags StateFlags;
}
}

View file

@ -1,10 +0,0 @@
using System;
namespace Ryujinx.Graphics.Gpu.Shader.Cache.Definition
{
[Flags]
enum GuestGpuStateFlags : byte
{
EarlyZForce = 1 << 0
}
}

Some files were not shown because too many files have changed in this diff Show more