NVN and AudioOut fixes from PRs #105, #107, #108, #109 and #110 (general, found on Luigi's Mansion 3): no blending on integer targets, primitive restart in fan and quad expansion, partial depth-stencil clears, every name of an upgraded texture, the SDK's AudioOut fields. nvn_mtl.m pso_desc (#105): an integer colour target took the game's blend enable, and Metal refuses such a pipeline with an assertion even without the API validation layer (checked on this Mac); Maxwell ignores blending on integer targets, so it is off for them. draw (#110): indexed triangle fans and quads expanded on the CPU read the restart index as a vertex and joined the polygons across it, and the extra triangles cleared parts of Luigi's Mansion 3's cached shadow depth; nvn_primitive.h ends a polygon at the restart index (compared before the base vertex), at most as many triangles as before. OP_CLEAR_DS (#108): a depth-stencil clear whose scissor does not cover the target, or whose stencil mask is partial, cleared the whole target before the first draw, and a partial stencil mask did so in a pass too; both are drawn as a triangle now (clear_ds_tri), full clears stay load actions. Logs on the T7 show who takes the new path: Tears of the Kingdom clears 8x8 tiles of a 64x64 depth target (each clear wiped the whole target), AI: THE SOMNIUM FILES clears 1280x720 of a 1920x1080 one. tex_upgrade (#107): a texture recreated to add a usage replaced only the asking serial and one sharing-table entry, so other serials on the same storage read the old copy; every entry in g_by_serial, g_ghost and g_alias now moves to the new texture in place, g_alias_set follows, and the old one is retired after the submissions that used it. hle_audio.c (#109, reworked): the host left the guest AudioOut zero, and middleware that checks its first word before Stop / Close skipped the close, freed its PCM and the host's consumer read freed memory (SIGBUS on resume in Luigi's Mansion 3). The PR wrote 1 and cleared it on close; the SDK's OpenAudioOut (SDK 8, 12, 18 and 20 checked) writes sample rate, channel count, sample format and state at +0 / +4 / +8 / +0xc, its getters load from there, and CloseAudioOut only sets the state to stopped, so the host now writes the same (au_info). New tests t_integer_blend (63 checks), t_primitive_restart (37), t_clear_ds (43), t_upgrade_alias (10) in the gfx run list, and an AudioOut fields and lifetime case in t_audio (12 guarded open / close / unmap / resume cycles, silent sink and real output). Checked: the four new tests pass under Metal validation; every other gfx test gives the same result as on main (t_fuse_check, t_reorder_chain, t_defer_chain, t_mip_extent, t_rt_range_ro and t_rt_range fail the same cases on both; t_refl finds no reflection samples in this checkout's shader cache); the host unit tests pass; on the Mac host for 90 s from boot, against main built the same way: Breath of the Wild over the title, 60 fps and the same red lines; AI: THE SOMNIUM FILES (shaders built by the NSP script on this chain), whose 1280x720 depth clears at the start of a pass now take the triangle path too (from pass 119), 60 fps, no red line, 5 long frames against 12. Not taken from this batch: #106 (the stencil-only view of a depth-stencil texture works without format-view usage; only the validation layer asks for it, and the PR would put that usage on every depth-stencil buffer), #111 (an opt-in draft with no memory saving shown), #104 (Mac app bundle)
Co-authored-by: João Karalkovas <117093313+Jpkovas@users.noreply.github.com>
This commit is contained in:
1 parent
6034fcad89
commit
bc5ba1b1c2
11 files changed
+354
-23
No files matched your search
+1
-1
@@ -75,7 +75,7 @@ Binding conventions followed during translation (the NVN driver's layout, the sa
|
||||
| `nvn/nvn_tex.c`, `nvn/nvn_tex.h` | Storage size, alignment and per-level offsets of block-linear textures (ported from deko3d) |
|
||||
| `nvn/nvn_dirty.c`, `nvn/nvn_dirty.h` | Tracks dirty pages of game memory with write protection, to decide whether texture contents have changed and need re-uploading |
|
||||
| `nvn/nvn_refl.m`, `nvn/nvn_refl.h` | Converts shader reflection json into fixed-length C arrays, so that drawing only walks arrays |
|
||||
| `nvn/nvn_hmap.h`, `nvn/nvn_cover.h`, `nvn/nvn_pace.h`, `nvn/nvn_pso_skey.h` | Hash table, geometric proof of full-coverage draws, presentation pacer, pipeline manifest key |
|
||||
| `nvn/nvn_hmap.h`, `nvn/nvn_cover.h`, `nvn/nvn_pace.h`, `nvn/nvn_pso_skey.h`, `nvn/nvn_primitive.h` | Hash table, geometric proof of full-coverage draws, presentation pacer, pipeline manifest key, indexed triangle fans and quads as triangle lists (primitive restart kept) |
|
||||
| `nvn/nvn_builtin.h` | Hand-written MSL for the 4 compute shaders embedded in the main program |
|
||||
| `nvn/include/nvn.h` | NVN interface declarations (third party; for the license see the file header) |
|
||||
| `nvn/names.txt` | The NVN API name list. The per-game tables described above live in the game profile `dm/games/<id>/` (named after the base game's title ID; tools pick it with `DM_GAME`, make with `GAME`; there is no default game) |
|
||||
|
||||
+33
-18
@@ -23,6 +23,7 @@
|
||||
#include "nvn_dirty.h"
|
||||
#include "nvn_refl.h"
|
||||
#include "nvn_cover.h"
|
||||
#include "nvn_primitive.h"
|
||||
#include "dmsh_chain.h" // DMSH_CHAIN: the translation chain version of the B shader library expected by this source tree, one for every game (beside this file, tools/dmsh_chain.py --header)
|
||||
#include "game_profile.h" // game profile constants (profile id, the profile's switches)
|
||||
// Texture slot table per stage: 0-127 the combined handles (nvnCommandBufferBindTexture), 128-255 the separate texture binds (NVN_SEPTEX_SLOT0, nvn_obj.h)
|
||||
@@ -1480,9 +1481,25 @@ static id<MTLTexture> tex_upgrade(const tex_t *t, id<MTLTexture> base, MTLTextur
|
||||
if ([holder isKindOfClass:[NvPkg class]]) { id<MTLTexture> old = ((NvPkg *)holder)->tex; ((NvPkg *)holder)->tex = upgraded; defer_obj_release(old); g_ser_gen++;
|
||||
if (((NvPkg *)holder)->view) { defer_obj_unref(((NvPkg *)holder)->view); ((NvPkg *)holder)->view = nil; ((NvPkg *)holder)->vbase = 0; } } // a sparse texture's view of the old one: bound whole until its next upload
|
||||
else {
|
||||
tex_put(t->serial, upgraded);
|
||||
alias_key ak; memset(&ak, 0, sizeof ak); ak.pool = (uint64_t)(uintptr_t)t->pool; ak.off = t->off; ak.d = t->d; // Switch the sharing table to the upgraded one too, so later objects on the same storage get it directly
|
||||
pthread_mutex_lock(&g_smu); if (hm_get_id(&g_alias, &ak) == base) { hm_put_id(&g_alias, &ak, upgraded); CFSetAddValue(g_alias_set, (__bridge const void *)upgraded); } pthread_mutex_unlock(&g_smu);
|
||||
// Every name of the old texture moves to the upgraded one: other serials sharing its storage (live in g_by_serial, finalized in g_ghost) and the sharing table. Only the asking serial moved before,
|
||||
// so the others kept reading the old copy while writes went to the new one, and the old texture stayed in g_alias_set. Slots are replaced in place (no insert, so no table resize during the walk);
|
||||
// the old texture is retired once the submissions that used it complete, the view caches follow g_ser_gen (PR #107)
|
||||
pthread_mutex_lock(&g_smu);
|
||||
nvn_hmap *tables[] = {&g_by_serial, &g_ghost}; int named = 0, shared = 0;
|
||||
for (unsigned j = 0; j < 2; j++) for (uint32_t i = 0; i < tables[j]->cap; i++) {
|
||||
if (tables[j]->vals[i] != (__bridge void *)base) continue;
|
||||
uint64_t serial; memcpy(&serial, tables[j]->keys + (size_t)i * tables[j]->klen, sizeof serial);
|
||||
void *old = tables[j]->vals[i]; tables[j]->vals[i] = (void *)CFBridgingRetain(upgraded); CFRelease(old); named |= serial == t->serial;
|
||||
}
|
||||
for (uint32_t i = 0; i < g_alias.cap; i++) {
|
||||
if (g_alias.vals[i] != (__bridge void *)base) continue;
|
||||
void *old = g_alias.vals[i]; g_alias.vals[i] = (void *)CFBridgingRetain(upgraded); CFRelease(old); shared = 1;
|
||||
}
|
||||
if (shared) { CFSetRemoveValue(g_alias_set, (__bridge const void *)base); CFSetAddValue(g_alias_set, (__bridge const void *)upgraded); }
|
||||
g_ser_gen++;
|
||||
pthread_mutex_unlock(&g_smu);
|
||||
if (!named) tex_put(t->serial, upgraded);
|
||||
defer_obj_release(base);
|
||||
}
|
||||
MLOG("texture upgraded to allow %s ser=%llu base format %lu → view format %lu (upgrades=%llu)", add & MTLTextureUsageRenderTarget ? "render-target use" : "format views", t->serial, (unsigned long)base.pixelFormat, (unsigned long)view_pf, ++g_pfv_upgrades);
|
||||
return upgraded;
|
||||
@@ -4059,6 +4076,7 @@ static int pso_all_uint(const pso_key *k, NvShader *fs, int merged) {
|
||||
}
|
||||
return n > 0;
|
||||
}
|
||||
static int pf_int_kind(MTLPixelFormat f);
|
||||
static MTLRenderPipelineDescriptor *pso_desc(const pso_key *k, NvShader *vs, NvShader *fs, id<MTLFunction> fsfn, int merged) {
|
||||
MTLRenderPipelineDescriptor *d = [MTLRenderPipelineDescriptor new];
|
||||
d.vertexFunction = vs->fn; d.fragmentFunction = fsfn ? fsfn : fs ? fs->fn : nil;
|
||||
@@ -4093,7 +4111,8 @@ static MTLRenderPipelineDescriptor *pso_desc(const pso_key *k, NvShader *vs, NvS
|
||||
int unwritten = fs && fs->rf.color_known && !fsfn && l != 0xff && !(fs->rf.color_mask >> l & 1);
|
||||
if (unwritten) m = 0;
|
||||
c.writeMask = (m & 1 ? MTLColorWriteMaskRed : 0) | (m & 2 ? MTLColorWriteMaskGreen : 0) | (m & 4 ? MTLColorWriteMaskBlue : 0) | (m & 8 ? MTLColorWriteMaskAlpha : 0);
|
||||
if (l != 0xff && k->blend_en >> l & 1 && !unwritten) {
|
||||
// Integer targets: no blending. Maxwell ignores the blend enable on them; Metal refuses the pipeline (an assertion even without the API validation layer): Luigi's Mansion 3's integer G-buffer (PR #105)
|
||||
if (l != 0xff && k->blend_en >> l & 1 && !unwritten && !pf_int_kind(c.pixelFormat)) {
|
||||
c.blendingEnabled = YES;
|
||||
c.sourceRGBBlendFactor = bfac(k->blend[l].src_rgb); c.destinationRGBBlendFactor = bfac(k->blend[l].dst_rgb);
|
||||
c.sourceAlphaBlendFactor = bfac(k->blend[l].src_a); c.destinationAlphaBlendFactor = bfac(k->blend[l].dst_a);
|
||||
@@ -6934,15 +6953,12 @@ static void draw(const p_draw *dw, const p_draw_idx *di) {
|
||||
if (!mtl_addr(di->ibuf, (uint64_t)di->count * isz, &mb, &mo) || !(src = [(__bridge id<MTLBuffer>)mb contents])) { RED_ONCE(22, "index buffer is in no memory pool"); return; }
|
||||
if (pend_hits(di->ibuf, (uint64_t)di->count * isz)) RED_ONCE(73, "triangle fan / quad index buffer has GPU-side writes in this submission: expanded from the current content");
|
||||
src += mo;
|
||||
#define IX(i) (isz == 1 ? (uint32_t)src[i] : isz == 2 ? (uint32_t)((const uint16_t *)src)[i] : ((const uint32_t *)src)[i])
|
||||
id<MTLBuffer> ab; size_t off = arena_alloc(((size_t)nt * 12 + 15) & ~(size_t)15, &ab);
|
||||
uint32_t *d = (uint32_t *)((uint8_t *)ab.contents + off);
|
||||
for (uint32_t t = 0; t < nt; t++) {
|
||||
if (prim == 6) { d[3 * t] = IX(0); d[3 * t + 1] = IX(t + 1); d[3 * t + 2] = IX(t + 2); }
|
||||
else { uint32_t q = (t >> 1) * 4, h = t & 1; d[3 * t] = IX(q); d[3 * t + 1] = IX(q + 1 + h); d[3 * t + 2] = IX(q + 2 + h); }
|
||||
}
|
||||
#undef IX
|
||||
enc_r_draw_indexed(en, mp, nt * 3, MTLIndexTypeUInt32, g_cur_arena->gpu + off, (NSUInteger)nt * 12, di->instances ? di->instances : 1, di->base_vertex, di->base_instance);
|
||||
// With primitive restart on, the restart index (compared before the base vertex is added) ends a fan or quad and the next one starts after it: they were joined across it,
|
||||
// and the extra triangles cleared parts of Luigi's Mansion 3's cached shadow depth (straight light edges on the garage floor, PR #110). At most nt triangles come out
|
||||
uint32_t count = nvn_expand_primitive(d, src, (uint32_t)di->count, (unsigned)isz, (unsigned)prim, S.restart.on, S.restart.index);
|
||||
if (count) enc_r_draw_indexed(en, mp, count, MTLIndexTypeUInt32, g_cur_arena->gpu + off, (NSUInteger)count * 4, di->instances ? di->instances : 1, di->base_vertex, di->base_instance);
|
||||
if (g_rt_range_pending) { PD("j rt_level_range pass=%llu vc=%u", g_stat_pass, (unsigned)di->count); g_rt_range_pending = 0; }
|
||||
return;
|
||||
}
|
||||
@@ -7653,17 +7669,16 @@ void mtl_exec(const uint8_t *p) {
|
||||
const p_clear_ds *c = a;
|
||||
if (g_rc_skip) { PD("c %llu depth-stencil clear skipped by conditional rendering", g_stat_pass); break; }
|
||||
if (g_rt_merged) rp_end();
|
||||
// Depth / stencil clear in the middle of a pass: end the current pass and attach the clear to the start of the next one (color attachments load as usual, as a continued pass does above when a clear arrives). It used to be skipped, and later draws depth-tested against stale depth.
|
||||
// When the scissor does not cover the depth target, Metal's load-action clear cannot express a partial clear: drawn as a triangle inside the pass (clear_ds_tri)
|
||||
int mid = g_enc != nil;
|
||||
// A load-action clear covers the whole attachment and every stencil bit. A clear whose scissor does not cover the depth target, or whose stencil mask is partial, is drawn as a triangle
|
||||
// (clear_ds_tri), in or before a pass: before the first draw and for a partial stencil mask in a pass it used to clear the whole target (PR #108)
|
||||
unsigned sm = (unsigned)c->stencil_mask & 0xff;
|
||||
if (R.rt.depth && (!scissor_full(R.rt.depth, &R.rt.dview, R.rt.has_dview) || (sm && sm != 0xff))) { clear_ds_tri(c); break; }
|
||||
// A full clear in the middle of a pass: end the pass and attach the clear to the start of the next one (color attachments load as usual, as a continued pass does above when a clear arrives).
|
||||
// It used to be skipped, and later draws depth-tested against stale depth
|
||||
if (g_enc) {
|
||||
if (R.rt.depth && !scissor_full(R.rt.depth, &R.rt.dview, R.rt.has_dview)) { clear_ds_tri(c); break; }
|
||||
{ static int once; if (!once++) MLOG("mid-pass depth-stencil clear: ending this pass; the clear moves to the start of the next pass (first case pass %llu)", g_stat_pass); }
|
||||
rp_end();
|
||||
}
|
||||
if (R.rt.depth && (!scissor_full(R.rt.depth, &R.rt.dview, R.rt.has_dview) || ((c->stencil_mask & 0xff) && (c->stencil_mask & 0xff) != 0xff))) {
|
||||
static int once; if (!once++) MLOG("depth-stencil clear with a scissor or partial stencil mask, cleared whole (first case pass %llu mid %d depth mask %d stencil mask %#x scissor %d,%d %dx%d target %ux%u)", g_stat_pass, mid, c->depth_mask, c->stencil_mask,
|
||||
R.sc.x, R.sc.y, R.sc.w, R.sc.h, R.rt.depth->d.width, R.rt.depth->d.height); }
|
||||
if (c->depth_mask) { R.clear_d = 1; R.cd = c->depth; }
|
||||
if (c->stencil_mask) { R.clear_s = 1; R.cs = (uint32_t)c->stencil; }
|
||||
PD("c %llu depth-stencil clear recorded ser=%llu depth=%g depth_mask=%d stencil=%d stencil_mask=%#x", g_stat_pass, R.rt.depth ? (unsigned long long)R.rt.depth->serial : 0ull, c->depth, c->depth_mask, c->stencil, c->stencil_mask);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Expand indexed fans/quads into triangle lists, respecting NVN primitive restart.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
static inline uint32_t nvn_expand_primitive(uint32_t *out, const uint8_t *src, uint32_t count,
|
||||
unsigned bytes, unsigned primitive, int restart, uint32_t restart_index) {
|
||||
uint32_t used = 0, have = 0, first = 0, previous = 0, quad[4];
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
uint32_t index = 0;
|
||||
if (bytes == 1) index = src[i];
|
||||
else if (bytes == 2) { uint16_t value; memcpy(&value, src + (size_t)i * 2, 2); index = value; }
|
||||
else memcpy(&index, src + (size_t)i * 4, 4);
|
||||
if (restart && index == restart_index) { have = 0; continue; }
|
||||
if (primitive == 6) {
|
||||
if (!have) first = index;
|
||||
else if (have >= 2) { out[used++] = first; out[used++] = previous; out[used++] = index; }
|
||||
previous = index;
|
||||
if (have < 2) have++;
|
||||
} else {
|
||||
quad[have++] = index;
|
||||
if (have == 4) {
|
||||
out[used++] = quad[0]; out[used++] = quad[1]; out[used++] = quad[2];
|
||||
out[used++] = quad[0]; out[used++] = quad[2]; out[used++] = quad[3];
|
||||
have = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return used;
|
||||
}
|
||||
@@ -64,3 +64,11 @@ t_linear_readback
|
||||
t_linear_readback.dSYM/
|
||||
t_pso_pending
|
||||
t_pso_pending.dSYM/
|
||||
t_integer_blend
|
||||
t_integer_blend.dSYM/
|
||||
t_primitive_restart
|
||||
t_primitive_restart.dSYM/
|
||||
t_clear_ds
|
||||
t_clear_ds.dSYM/
|
||||
t_upgrade_alias
|
||||
t_upgrade_alias.dSYM/
|
||||
+13
-1
@@ -93,10 +93,18 @@ check_t_linear_readback: t_linear_readback
|
||||
run: check_t_linear_readback
|
||||
t_pfv: t_pfv.m $(MTL_T_DEP)
|
||||
clang $(CFLAGS) -fobjc-arc -o $@ t_pfv.m $(MTL_T_SRC) $(FW)
|
||||
t_integer_blend: t_integer_blend.m $(MTL_T_DEP)
|
||||
clang $(CFLAGS) -fobjc-arc -o $@ t_integer_blend.m $(MTL_T_SRC) $(FW)
|
||||
t_primitive_restart: t_primitive_restart.m $(MTL_T_DEP)
|
||||
clang $(CFLAGS) -fobjc-arc -o $@ t_primitive_restart.m $(MTL_T_SRC) $(FW)
|
||||
t_clear_ds: t_clear_ds.m $(MTL_T_DEP)
|
||||
clang $(CFLAGS) -fobjc-arc -o $@ t_clear_ds.m $(MTL_T_SRC) $(FW)
|
||||
t_upgrade_alias: t_upgrade_alias.m $(MTL_T_DEP)
|
||||
clang $(CFLAGS) -fobjc-arc -o $@ t_upgrade_alias.m $(MTL_T_SRC) $(FW)
|
||||
# The Vulkan layer (#included) over the whole NVN layer, whose device and present path it uses
|
||||
t_vk_desc: t_vk_desc.m ../vk/hle_vk.m $(MTL_T_DEP)
|
||||
clang $(CFLAGS) -I../../third_party/vulkan-headers/include -fobjc-arc -o $@ t_vk_desc.m $(MTL_T_SRC) ../nvn/nvn_mtl.m $(FW)
|
||||
run: t_nvn t_dirty_slots t_dirty_race t_builtin t_vin t_tbuf2d t_deswz_gpu t_pace t_fstep t_refl t_hmap t_cover t_fuse_check t_present_crop t_reorder_chain t_defer_chain t_zskip t_xv t_ub t_vbuf t_geometry t_mip_extent t_rt_range_shape t_rt_range_ro t_rt_range t_rt_depth_ro t_pso_skey t_pso_pending t_pfv t_view_levels t_view_swizzle t_vk_desc
|
||||
run: t_nvn t_dirty_slots t_dirty_race t_builtin t_vin t_tbuf2d t_deswz_gpu t_pace t_fstep t_refl t_hmap t_cover t_fuse_check t_present_crop t_reorder_chain t_defer_chain t_zskip t_xv t_ub t_vbuf t_geometry t_mip_extent t_rt_range_shape t_rt_range_ro t_rt_range t_rt_depth_ro t_pso_skey t_pso_pending t_pfv t_view_levels t_view_swizzle t_vk_desc t_integer_blend t_primitive_restart t_clear_ds t_upgrade_alias
|
||||
./t_nvn
|
||||
./t_dirty_slots
|
||||
./t_dirty_race
|
||||
@@ -135,5 +143,9 @@ run: t_nvn t_dirty_slots t_dirty_race t_builtin t_vin t_tbuf2d t_deswz_gpu t_pac
|
||||
MTL_DEBUG_LAYER=1 ./t_view_levels
|
||||
MTL_DEBUG_LAYER=1 ./t_view_swizzle
|
||||
MTL_DEBUG_LAYER=1 ./t_vk_desc
|
||||
MTL_DEBUG_LAYER=1 ./t_integer_blend
|
||||
MTL_DEBUG_LAYER=1 ./t_primitive_restart
|
||||
MTL_DEBUG_LAYER=1 ./t_clear_ds
|
||||
MTL_DEBUG_LAYER=1 ./t_upgrade_alias
|
||||
DM_GAME=$(GAME) python3 test_tbdr_budget.py
|
||||
.PHONY: run
|
||||
@@ -0,0 +1,73 @@
|
||||
// Partial depth/stencil clears preserve every pixel and stencil bit outside their mask.
|
||||
#include "../nvn/nvn_mtl.m"
|
||||
void *front_metal_layer(void) { return NULL; }
|
||||
module_t *module_of(const void *p) { (void)p; return NULL; }
|
||||
module_t g_mod[MAX_MOD]; int g_nmod;
|
||||
void svc_thread_dump(void) {}
|
||||
uint64_t now_ns(void) { return bp_now(); }
|
||||
static int checks, failed;
|
||||
#define CHECK(c, label) do { checks++; if (!(c)) { failed++; fprintf(stderr, "FAIL: %s\n", label); } } while (0)
|
||||
|
||||
int main(void) { @autoreleasepool {
|
||||
mtl_init();
|
||||
NSError *error = nil;
|
||||
id<MTLLibrary> library = [g_dev newLibraryWithSource:@"#include <metal_stdlib>\nusing namespace metal;\n"
|
||||
"kernel void read_planes(texture2d<uint,access::read> s [[texture(0)]], depth2d<float,access::read> d [[texture(1)]], device uint2 *o [[buffer(0)]], uint2 p [[thread_position_in_grid]]) { o[p.y*8+p.x]=uint2(s.read(p).r, as_type<uint>(d.read(p))); }\n" options:nil error:&error];
|
||||
id<MTLComputePipelineState> reader = [g_dev newComputePipelineStateWithFunction:[library newFunctionWithName:@"read_planes"] error:&error];
|
||||
CHECK(reader != nil, "synthetic plane reader compiles");
|
||||
if (!reader) return 1;
|
||||
id<MTLCommandQueue> queue = [g_dev newCommandQueue];
|
||||
struct { int mid; p_scissor sc; int depth, mask; const char *label; } cases[] = {
|
||||
{0, {2,1,3,2}, 1, 0, "outside-pass scissored depth clear"},
|
||||
{0, {2,1,3,2}, 0, 255, "outside-pass scissored stencil clear"},
|
||||
{0, {0,0,8,8}, 0, 15, "outside-pass masked stencil clear"},
|
||||
{1, {0,0,8,8}, 0, 15, "inside-pass masked stencil clear"},
|
||||
{1, {2,1,3,2}, 1, 15, "inside-pass scissored depth/stencil clear"},
|
||||
{0, {2,1,0,2}, 1, 255, "empty scissor writes nothing"},
|
||||
{0, {-1,-1,3,3}, 1, 15, "negative scissor origin is clamped"},
|
||||
{0, {0,0,8,8}, 1, 255, "full clear remains a load action"},
|
||||
};
|
||||
for (unsigned i=0; i<sizeof cases/sizeof *cases; i++) {
|
||||
tex_t t = {.magic=MAGIC('T','E','X',0), .serial=99401+i};
|
||||
t.d = (nvn_texdesc){.target=NVN_TEXTURE_TARGET_2D, .format=NVN_FORMAT_DEPTH32F_STENCIL8, .width=8, .height=8, .depth=1, .levels=1, .samples=1};
|
||||
MTLTextureDescriptor *descriptor = tex_desc(&t, 0); descriptor.usage |= MTLTextureUsagePixelFormatView;
|
||||
id<MTLTexture> base = [g_dev newTextureWithDescriptor:descriptor];
|
||||
CHECK(base != nil, "synthetic depth/stencil target allocates");
|
||||
if (!base) continue;
|
||||
MTLRenderPassDescriptor *init = [MTLRenderPassDescriptor renderPassDescriptor];
|
||||
init.depthAttachment.texture = init.stencilAttachment.texture = base;
|
||||
init.depthAttachment.loadAction = init.stencilAttachment.loadAction = MTLLoadActionClear;
|
||||
init.depthAttachment.storeAction = init.stencilAttachment.storeAction = MTLStoreActionStore;
|
||||
init.depthAttachment.clearDepth = .75; init.stencilAttachment.clearStencil = 0xa5;
|
||||
id<MTLCommandBuffer> cb = [queue commandBuffer]; [[cb renderCommandEncoderWithDescriptor:init] endEncoding]; [cb commit]; [cb waitUntilCompleted];
|
||||
CHECK(cb.status == MTLCommandBufferStatusCompleted, "literal initial planes reach the GPU");
|
||||
res_add(base); mtl_tex_init(&t); serial_put(t.serial, base);
|
||||
state_reset(); memset(&R,0,sizeof R); mtl_sub_begin();
|
||||
R.has_rt=1; R.rt.depth=&t; R.has_sc=1; R.sc=cases[i].sc;
|
||||
if (cases[i].mid) CHECK(rp_begin() != nil, "preexisting render pass opens");
|
||||
struct { op_hdr head; p_clear_ds clear; op_hdr end; } ops = {0};
|
||||
ops.head=(op_hdr){OP_CLEAR_DS, sizeof(op_hdr)+sizeof(p_clear_ds)};
|
||||
ops.clear=(p_clear_ds){.depth=.25, .depth_mask=cases[i].depth, .stencil=0x3c, .stencil_mask=cases[i].mask};
|
||||
ops.end=(op_hdr){OP_END,sizeof(op_hdr)};
|
||||
mtl_exec((const uint8_t *)&ops); mtl_sub_commit(); mtl_drain();
|
||||
CHECK(!mtl_last_commit_error(), "production clear completes without GPU error");
|
||||
id<MTLTexture> stencil = [base newTextureViewWithPixelFormat:MTLPixelFormatX32_Stencil8];
|
||||
id<MTLBuffer> result = [g_dev newBufferWithLength:64*8 options:MTLResourceStorageModeShared];
|
||||
cb=[queue commandBuffer]; id<MTLComputeCommandEncoder> e=[cb computeCommandEncoder];
|
||||
[e setComputePipelineState:reader]; [e setTexture:stencil atIndex:0]; [e setTexture:base atIndex:1]; [e setBuffer:result offset:0 atIndex:0];
|
||||
[e dispatchThreads:MTLSizeMake(8,8,1) threadsPerThreadgroup:MTLSizeMake(8,8,1)]; [e endEncoding]; [cb commit]; [cb waitUntilCompleted];
|
||||
CHECK(cb.status == MTLCommandBufferStatusCompleted, "both resulting planes are read on the GPU");
|
||||
uint32_t *pixels=result.contents; int valid=1;
|
||||
for (int y=0;y<8;y++) for (int x=0;x<8;x++) {
|
||||
p_scissor sc=cases[i].sc; int inside=x>=sc.x && x<sc.x+sc.w && y>=sc.y && y<sc.y+sc.h;
|
||||
unsigned mask=inside?cases[i].mask:0, want_s=(0xa5&~mask)|(0x3c&mask);
|
||||
uint32_t want_d=inside&&cases[i].depth?0x3e800000:0x3f400000;
|
||||
valid &= pixels[2*(y*8+x)]==want_s && pixels[2*(y*8+x)+1]==want_d;
|
||||
}
|
||||
CHECK(valid,cases[i].label);
|
||||
printf("case %u: %s %s\n",i,cases[i].label,valid?"PASS":"FAIL");
|
||||
mtl_tex_release(&t);
|
||||
}
|
||||
printf("partial depth/stencil clears: %d checks, %d failed\n",checks,failed);
|
||||
return failed!=0;
|
||||
} }
|
||||
@@ -0,0 +1,71 @@
|
||||
// Integer color attachments ignore blending; the same descriptor must remain valid on Metal.
|
||||
#include "../nvn/nvn_mtl.m"
|
||||
|
||||
void *front_metal_layer(void) { return NULL; }
|
||||
module_t *module_of(const void *p) { (void)p; return NULL; }
|
||||
module_t g_mod[MAX_MOD]; int g_nmod;
|
||||
void svc_thread_dump(void) {}
|
||||
uint64_t now_ns(void) { return bp_now(); }
|
||||
|
||||
static int checks, failed;
|
||||
#define CHECK(c, label) do { checks++; if (!(c)) { failed++; fprintf(stderr, "FAIL: %s\n", label); } } while (0)
|
||||
|
||||
int main(void) { @autoreleasepool {
|
||||
mtl_init();
|
||||
NvShader *vs = [NvShader new], *fs = [NvShader new];
|
||||
fs->rf.color_known = 1; fs->rf.color_mask = 3;
|
||||
pso_key k = {0}; k.samples = 1; k.chmask = 0xff; k.blend_en = 3;
|
||||
k.blend[0].src_rgb = k.blend[0].src_a = 1;
|
||||
k.blend[1] = k.blend[0];
|
||||
MTLPixelFormat integers[] = {MTLPixelFormatR8Uint, MTLPixelFormatR8Sint, MTLPixelFormatR16Uint, MTLPixelFormatR16Sint,
|
||||
MTLPixelFormatR32Uint, MTLPixelFormatR32Sint, MTLPixelFormatRG8Uint, MTLPixelFormatRG8Sint,
|
||||
MTLPixelFormatRG16Uint, MTLPixelFormatRG16Sint, MTLPixelFormatRG32Uint, MTLPixelFormatRG32Sint,
|
||||
MTLPixelFormatRGBA8Uint, MTLPixelFormatRGBA8Sint, MTLPixelFormatRGB10A2Uint,
|
||||
MTLPixelFormatRGBA16Uint, MTLPixelFormatRGBA16Sint, MTLPixelFormatRGBA32Uint, MTLPixelFormatRGBA32Sint};
|
||||
for (unsigned i = 0; i < sizeof integers / sizeof *integers; i++) {
|
||||
k.cfmt[0] = integers[i]; k.cfmt[1] = MTLPixelFormatRGBA8Unorm;
|
||||
MTLRenderPipelineDescriptor *d = pso_desc(&k, vs, fs, nil, 0);
|
||||
CHECK(!d.colorAttachments[0].blendingEnabled, "integer attachment bypasses blending");
|
||||
CHECK(d.colorAttachments[0].writeMask == MTLColorWriteMaskAll, "integer output remains writable");
|
||||
CHECK(d.colorAttachments[1].blendingEnabled, "normalized attachment retains requested blending");
|
||||
}
|
||||
k.cfmt[0] = MTLPixelFormatR32Uint; k.cfmt[1] = MTLPixelFormatRGBA8Uint;
|
||||
MTLRenderPipelineDescriptor *d = pso_desc(&k, vs, fs, nil, 0);
|
||||
CHECK(!d.colorAttachments[0].blendingEnabled && !d.colorAttachments[1].blendingEnabled,
|
||||
"two integer attachments do not create an invalid pipeline");
|
||||
if (!failed) {
|
||||
NSString *source = @"#include <metal_stdlib>\nusing namespace metal;\n"
|
||||
"vertex float4 v(uint i [[vertex_id]]) { return float4(i == 1 ? 3.0 : -1.0, i == 2 ? 3.0 : -1.0, 0, 1); }\n"
|
||||
"struct O { uint4 a [[color(0)]]; uint4 b [[color(1)]]; };\n"
|
||||
"fragment O f() { return {uint4(0x12345678u), uint4(17,34,51,68)}; }\n";
|
||||
NSError *error = nil;
|
||||
id<MTLLibrary> library = [g_dev newLibraryWithSource:source options:nil error:&error];
|
||||
CHECK(library != nil, "synthetic shader compiles");
|
||||
vs->fn = [library newFunctionWithName:@"v"]; fs->fn = [library newFunctionWithName:@"f"];
|
||||
d = pso_desc(&k, vs, fs, nil, 0);
|
||||
id<MTLRenderPipelineState> pipeline = [g_dev newRenderPipelineStateWithDescriptor:d error:&error];
|
||||
CHECK(pipeline != nil, "real Metal accepts the corrected descriptor");
|
||||
if (pipeline) {
|
||||
MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:k.cfmt[0] width:4 height:4 mipmapped:NO];
|
||||
td.usage = MTLTextureUsageRenderTarget; td.storageMode = MTLStorageModeShared;
|
||||
id<MTLTexture> a = [g_dev newTextureWithDescriptor:td]; td.pixelFormat = k.cfmt[1];
|
||||
id<MTLTexture> b = [g_dev newTextureWithDescriptor:td];
|
||||
MTLRenderPassDescriptor *rp = [MTLRenderPassDescriptor renderPassDescriptor];
|
||||
for (int i = 0; i < 2; i++) { rp.colorAttachments[i].texture = i ? b : a;
|
||||
rp.colorAttachments[i].loadAction = MTLLoadActionClear; rp.colorAttachments[i].storeAction = MTLStoreActionStore; }
|
||||
id<MTLCommandQueue> queue = [g_dev newCommandQueue]; id<MTLCommandBuffer> cb = [queue commandBuffer];
|
||||
id<MTLRenderCommandEncoder> encoder = [cb renderCommandEncoderWithDescriptor:rp];
|
||||
[encoder setRenderPipelineState:pipeline]; [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
|
||||
[encoder endEncoding]; [cb commit]; [cb waitUntilCompleted];
|
||||
CHECK(cb.status == MTLCommandBufferStatusCompleted, "integer render completes");
|
||||
uint32_t pixels[16]; [a getBytes:pixels bytesPerRow:16 fromRegion:MTLRegionMake2D(0,0,4,4) mipmapLevel:0];
|
||||
int ok = 1; for (int i = 0; i < 16; i++) ok &= pixels[i] == 0x12345678u;
|
||||
CHECK(ok, "R32Uint output preserves all literal bits");
|
||||
[b getBytes:pixels bytesPerRow:16 fromRegion:MTLRegionMake2D(0,0,4,4) mipmapLevel:0];
|
||||
ok = 1; for (int i = 0; i < 16; i++) ok &= pixels[i] == 0x44332211u;
|
||||
CHECK(ok, "RGBA8Uint output preserves four literal channels");
|
||||
}
|
||||
}
|
||||
printf("integer blending: %d checks, %d failed\n", checks, failed);
|
||||
return failed != 0;
|
||||
} }
|
||||
@@ -0,0 +1,39 @@
|
||||
// Indexed fans and quads keep restart-separated polygons apart on the real GPU.
|
||||
#include "../nvn/nvn_mtl.m"
|
||||
void *front_metal_layer(void) { return NULL; }
|
||||
module_t *module_of(const void *p) { (void)p; return NULL; }
|
||||
module_t g_mod[MAX_MOD]; int g_nmod;
|
||||
void svc_thread_dump(void) {}
|
||||
uint64_t now_ns(void) { return bp_now(); }
|
||||
static int checks, failed;
|
||||
#define CHECK(c,label) do { checks++; if (!(c)) { failed++; fprintf(stderr,"FAIL: %s\n",label); } } while(0)
|
||||
int main(void){@autoreleasepool{
|
||||
mtl_init();NSError *error=nil;
|
||||
id<MTLLibrary> lib=[g_dev newLibraryWithSource:@"#include <metal_stdlib>\nusing namespace metal;\n"
|
||||
"vertex float4 v(uint i [[vertex_id]]){const float2 p[8]={float2(-.9,-.8),float2(-.3,-.8),float2(-.3,.8),float2(-.9,.8),float2(.3,-.8),float2(.9,-.8),float2(.9,.8),float2(.3,.8)};return float4(p[i%8],.5,1);}\n"
|
||||
"fragment float4 f(){return float4(1,0,0,1);}\n" options:nil error:&error];
|
||||
CHECK(lib!=nil,"synthetic shaders compile");if(!lib)return 1;
|
||||
NvProg *pr=[NvProg new];pr->sh[0]=[NvShader new];pr->sh[1]=[NvShader new];
|
||||
pr->sh[0]->fn=[lib newFunctionWithName:@"v"];pr->sh[1]->fn=[lib newFunctionWithName:@"f"];pr->sh[1]->stage=1;
|
||||
pr->sh[1]->rf.color_known=1;pr->sh[1]->rf.color_mask=1;
|
||||
prog_t pg={.serial=99501};serial_put(pg.serial,pr);
|
||||
size_t page=(size_t)getpagesize();void *mem=NULL;if(posix_memalign(&mem,page,page))return 2;mtl_pool_add(mem,page);
|
||||
int test=0;
|
||||
for(int isz=1;isz<=4;isz*=2)for(int custom=0;custom<2;custom++)for(int prim=6;prim<=7;prim++){
|
||||
uint32_t restart=custom?13:isz==1?255:isz==2?65535:UINT32_MAX;
|
||||
uint32_t indices[]={restart,0,1,restart,restart,0,1,2,3,restart,4,5,6,7,restart};
|
||||
for(unsigned i=0;i<sizeof indices/sizeof *indices;i++)memcpy((uint8_t*)mem+i*isz,&indices[i],isz);
|
||||
tex_t t={.magic=MAGIC('T','E','X',0),.serial=99600+test++};
|
||||
t.d=(nvn_texdesc){.target=NVN_TEXTURE_TARGET_2D,.format=NVN_FORMAT_RGBA8,.width=32,.height=16,.depth=1,.levels=1,.samples=1};
|
||||
MTLTextureDescriptor *td=tex_desc(&t,0);td.storageMode=MTLStorageModeShared;id<MTLTexture>out=[g_dev newTextureWithDescriptor:td];res_add(out);mtl_tex_init(&t);serial_put(t.serial,out);
|
||||
state_reset();memset(&R,0,sizeof R);mtl_sub_begin();R.has_rt=1;R.rt.n=1;R.rt.color[0]=&t;R.clear_c[0]=1;R.cc[0]=MTLClearColorMake(0,0,0,0);
|
||||
S.prog=&pg;S.ds.dt=S.ds.dw=S.ds.st=0;S.poly.cull=0;S.color.blend_en=0;S.chmask.mask=0xffff;S.restart=(p_prim_restart){1,restart};
|
||||
p_draw_idx di={.prim=prim,.count=sizeof indices/sizeof *indices,.itype=isz==1?0:isz==2?1:2,.ibuf=(uint64_t)mem,.instances=1};draw(NULL,&di);
|
||||
uint64_t fence=mtl_sub_commit();CHECK(mtl_wait(fence,5000000000ull),"restart draw completes");CHECK(!mtl_last_commit_error(),"restart draw has no GPU error");
|
||||
uint8_t pixels[32*16*4];[out getBytes:pixels bytesPerRow:32*4 fromRegion:MTLRegionMake2D(0,0,32,16) mipmapLevel:0];
|
||||
int good=1;for(int y=3;y<13;y++)for(int x=0;x<32;x++){int lit=(x>=2&&x<11)||(x>=21&&x<30);good&=(pixels[(y*32+x)*4]>127)==lit;}
|
||||
if (!good) { fprintf(stderr,"row8:"); for(int x=0;x<32;x++) fprintf(stderr,"%c",pixels[(8*32+x)*4]>127?'#':'.'); fputc('\n',stderr); }
|
||||
CHECK(good,"two polygons lit, gap and outside untouched");printf("indices=%d custom=%d prim=%d %s\n",isz,custom,prim,good?"PASS":"FAIL");mtl_tex_release(&t);
|
||||
}
|
||||
mtl_pool_del(mem);mtl_drain();free(mem);printf("primitive restart: %d checks, %d failed\n",checks,failed);return failed!=0;
|
||||
}}
|
||||
@@ -0,0 +1,46 @@
|
||||
// A usage upgrade replaces one shared image, including every serial naming it.
|
||||
#include "../nvn/nvn_mtl.m"
|
||||
void *front_metal_layer(void) { return NULL; }
|
||||
module_t *module_of(const void *p) { (void)p; return NULL; }
|
||||
module_t g_mod[MAX_MOD]; int g_nmod;
|
||||
void svc_thread_dump(void) {}
|
||||
uint64_t now_ns(void) { return bp_now(); }
|
||||
static int checks, failed;
|
||||
#define CHECK(c, label) do { checks++; if (!(c)) { failed++; fprintf(stderr, "FAIL: %s\n", label); } } while (0)
|
||||
int main(void) { @autoreleasepool {
|
||||
mtl_init(); mtl_sub_begin();
|
||||
tex_t a = {.magic=MAGIC('T','E','X',0), .serial=99301};
|
||||
a.d = (nvn_texdesc){.target=NVN_TEXTURE_TARGET_2D, .format=NVN_FORMAT_RGBA8, .width=4, .height=4, .depth=1, .levels=1, .samples=1};
|
||||
nvn_tex_layout(&a.d, &a.L); tex_t b = a, ghost = a;
|
||||
b.serial++; ghost.serial += 2;
|
||||
mtl_tex_init(&a); mtl_tex_init(&b);
|
||||
id<MTLTexture> old = tex_mtl(&a);
|
||||
CHECK(tex_mtl(&b) == old && tex_mtl(&ghost) == old, "live and finalized serials initially share storage");
|
||||
// Exercise replacement at the hash table's growth threshold: reinserting while iterating would resize and reshuffle it.
|
||||
uint64_t extra = 99400;
|
||||
while ((g_by_serial.n + 1) * 4 <= g_by_serial.cap * 3) { hm_put_id(&g_by_serial, &extra, old); extra++; }
|
||||
uint32_t capacity = g_by_serial.cap, count = g_by_serial.n;
|
||||
id<MTLTexture> upgraded = tex_upgrade_pfv(&a, old, MTLPixelFormatR32Uint);
|
||||
CHECK(upgraded && upgraded != old, "usage upgrade creates a replacement");
|
||||
CHECK(g_by_serial.cap == capacity && g_by_serial.n == count, "replacement does not grow or reshape the serial table");
|
||||
int all = 1; for (uint64_t serial = 99400; serial < extra; serial++) all &= tex_get(serial) == upgraded;
|
||||
CHECK(all, "every shared serial at the growth threshold sees the replacement");
|
||||
CHECK(tex_get(b.serial) == upgraded, "other live serial sees the replacement");
|
||||
CHECK(tex_get(ghost.serial) == upgraded, "finalized serial sees the replacement");
|
||||
CHECK(!alias_has(old) && alias_has(upgraded), "sharing membership follows the replacement");
|
||||
uint32_t pixels[16]; for (int i = 0; i < 16; i++) pixels[i] = 0x44332211u;
|
||||
id<MTLBuffer> src = [g_dev newBufferWithBytes:pixels length:sizeof pixels options:MTLResourceStorageModeShared]; res_add(src);
|
||||
id<MTL4ComputeCommandEncoder> encoder = blit();
|
||||
enc_b_copy_buffer_texture(encoder, src, 0, 16, sizeof pixels, MTLSizeMake(4,4,1), upgraded, 0, 0, MTLOriginMake(0,0,0));
|
||||
for (int j = 0; j < 2; j++) {
|
||||
id<MTLTexture> texture = tex_mtl(j ? &ghost : &b);
|
||||
id<MTLBuffer> dst = [g_dev newBufferWithLength:sizeof pixels options:MTLResourceStorageModeShared]; res_add(dst);
|
||||
encoder = blit(); enc_b_copy_texture_buffer(encoder, texture, 0, 0, MTLOriginMake(0,0,0), MTLSizeMake(4,4,1), dst, 0, 16, sizeof pixels);
|
||||
mtl_sub_commit(); mtl_drain();
|
||||
CHECK(!memcmp(dst.contents, pixels, sizeof pixels), "other serial reads every GPU-written pixel after upgrade");
|
||||
if (!j) mtl_sub_begin();
|
||||
}
|
||||
CHECK(![g_res_objects containsObject:old], "retired base is removed from the residency set after GPU completion");
|
||||
printf("texture upgrade aliases: %d checks, %d failed\n", checks, failed);
|
||||
return failed != 0;
|
||||
} }
|
||||
+8
-2
@@ -1,7 +1,7 @@
|
||||
// nn::audio AudioOut: the game (FMOD nx output layer) appends PCM buffers block by block; once played the system puts them back on the "released" queue and signals the event.
|
||||
// Host: real audio output = CoreAudio AudioUnit (Mac DefaultOutput / iOS RemoteIO); silent sink = consumes buffers in real time (no sound, pacing matches the real device).
|
||||
// iOS defaults to real audio output (the product must have sound), tests that want silence set DM_AUDIO=0 explicitly; Mac defaults to the silent sink (replay / gate checks are silent), DM_AUDIO=1 for real audio output.
|
||||
// The contents of AudioOut / AudioOutBuffer / AudioOutParameter are read and written only through the interfaces below; the host defines its own internal layout and only writes the front part of the structs.
|
||||
// The contents of AudioOut / AudioOutBuffer / AudioOutParameter are read and written only through the interfaces below; the host defines its own internal layout and only writes the front part of the structs (AudioOut: the SDK's own fields, au_info).
|
||||
#include "hle.h"
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <TargetConditionals.h>
|
||||
@@ -267,6 +267,10 @@ HLE_FORCE("_ZN2nn5audio13ListAudioOutsEPNS0_12AudioOutInfoEi", "audio output dev
|
||||
if (n < 1) return 0;
|
||||
memset(info, 0, 256); strcpy(info, "DeviceOut"); return 1;
|
||||
}
|
||||
// The front of the guest AudioOut as the SDK's OpenAudioOut fills it from the service's answer: sample rate, channel count, sample format, state (+0 / +4 / +8 / +0xc, the same in SDK 8, 12, 18 and 20;
|
||||
// its GetAudioOutSampleRate / ChannelCount / SampleFormat are single loads from there); CloseAudioOut sets the state to stopped and leaves the rest. Code outside the SDK reads these fields too:
|
||||
// middleware that checks the first word before it stops and closes its output skipped the close while the host left it zero, freed the PCM buffers and the host's consumer read freed memory (Luigi's Mansion 3, PR #109)
|
||||
static void au_info(void *g, int ch, int state) { uint32_t *f = g; f[0] = RATE; f[1] = (uint32_t)ch; f[2] = FMT_PCM16; f[3] = (uint32_t)state; }
|
||||
static Result au_open_at(void *g, void *ev, const char *name, const uint8_t *param, void *lr) {
|
||||
static _Atomic int nopen;
|
||||
int ch = *(const uint16_t *)(param + 4); int rate = *(const int32_t *)param;
|
||||
@@ -291,6 +295,7 @@ static Result au_open_at(void *g, void *ev, const char *name, const uint8_t *par
|
||||
char sb[64];
|
||||
LOG("audio #%d OpenAudioOut %s channels %d %s%s (call site %s)", a->id, name ? name : "(default)", ch, real_audio() ? "real output" : "silent sink", ev ? "" : ", no release event (the game polls)", site(lr, sb, sizeof sb));
|
||||
if (real_audio()) { static pthread_once_t wd = PTHREAD_ONCE_INIT; au_open(a); pthread_once(&wd, au_watchdog_start); }
|
||||
au_info(g, ch, STATE_STOPPED);
|
||||
return RES_OK;
|
||||
}
|
||||
HLE_FORCE("_ZN2nn5audio12OpenAudioOutEPNS0_8AudioOutEPNS_2os11SystemEventEPKcRKNS0_17AudioOutParameterE", "connect to the audout service; host AudioUnit / silent sink",
|
||||
@@ -335,7 +340,8 @@ static void au_close(void *g, void *lr) {
|
||||
if (a->au) { AudioUnitUninitialize(a->au); AudioComponentInstanceDispose(a->au); }
|
||||
dispatch_release(a->notify_sem);
|
||||
if (a->rel_sem) dispatch_release(a->rel_sem);
|
||||
pthread_mutex_destroy(&a->m); free(a);
|
||||
pthread_mutex_destroy(&a->m); int ch = a->ch; free(a);
|
||||
au_info(g, ch, STATE_STOPPED);
|
||||
}
|
||||
HLE_FORCE("_ZN2nn5audio13CloseAudioOutEPNS0_8AudioOutE", "close the audio output", void, h_au_Close, void *g) { au_close(g, __builtin_return_address(0)); }
|
||||
HLE_FORCE("_ZN2nn5audio16GetAudioOutStateEPKNS0_8AudioOutE", "playback state", int, h_au_State, const void *g) { return atomic_load_explicit(&ao(g)->state, memory_order_acquire); }
|
||||
|
||||
+33
-1
@@ -2,6 +2,7 @@
|
||||
// Expected values are computed directly from the sample rate and block length: 4800 frames / 48 kHz = 100 ms.
|
||||
#include "../hle_audio.c"
|
||||
#include <time.h>
|
||||
#include <sys/mman.h>
|
||||
uint64_t now_ns(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return (uint64_t)t.tv_sec * 1000000000ull + t.tv_nsec; }
|
||||
static struct { const void *k; void *v; } om[8];
|
||||
void *objmap_get(const void *k) { for (int i = 0; i < 8; i++) if (om[i].k == k) return om[i].v; return NULL; }
|
||||
@@ -45,8 +46,38 @@ static int test_surround_output(void) {
|
||||
puts("surround callback: all six channels audible, saturation and split-buffer release verified");
|
||||
return 0;
|
||||
}
|
||||
// The guest AudioOut's front as the SDK fills it (au_info), and its lifetime under a guard like the middleware's: open, play, go to the background, close only if the first word says open,
|
||||
// make the PCM inaccessible (a consumer left running would fault on it) and come back, 12 times (PR #109). Old pages stay mapped PROT_NONE, so no new buffer lands at a stale address
|
||||
static int test_output_lifetime(void) {
|
||||
enum { CYCLES = 12 };
|
||||
uint32_t out[16] = {0}; uint8_t prm[16];
|
||||
size_t page = (size_t)getpagesize(), bytes = page * 4;
|
||||
void *pcm[CYCLES]; gbuf *buf[CYCLES];
|
||||
h_au_InitParam(prm);
|
||||
for (int i = 0; i < CYCLES; i++) {
|
||||
pcm[i] = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
buf[i] = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (pcm[i] == MAP_FAILED || buf[i] == MAP_FAILED) return 1;
|
||||
memset(out, 0, sizeof out);
|
||||
if (h_au_OpenDef0(out, prm)) return 1;
|
||||
if (out[0] != RATE || out[1] != 2 || out[2] != FMT_PCM16 || out[3] != STATE_STOPPED) {
|
||||
printf("FAIL AudioOut open: guest fields %u %u %u %u, expected %u 2 %u %u as the SDK writes them\n", out[0], out[1], out[2], out[3], RATE, FMT_PCM16, STATE_STOPPED); return 1;
|
||||
}
|
||||
h_au_SetBuf(buf[i], pcm[i], bytes, bytes);
|
||||
if (!h_au_Append(out, buf[i])) return 1;
|
||||
h_au_Start(out); usleep(1000); dm_audio_suspend(1);
|
||||
if (out[0]) { if (h_au_State(out) == STATE_STARTED) h_au_Stop(out); h_au_Close(out); }
|
||||
if (out[0] != RATE || out[3] != STATE_STOPPED || objmap_get(out)) { puts("FAIL AudioOut close: state not stopped or the host output still open"); return 1; }
|
||||
for (int k = 0; k < AOMAX; k++) if (g_aos[k]) { puts("FAIL AudioOut close: a consumer is still registered"); return 1; }
|
||||
if (mprotect(pcm[i], bytes, PROT_NONE) || mprotect(buf[i], page, PROT_NONE)) return 1;
|
||||
dm_audio_suspend(0); usleep(10000);
|
||||
}
|
||||
for (int i = 0; i < CYCLES; i++) { munmap(pcm[i], bytes); munmap(buf[i], page); }
|
||||
printf("AudioOut fields and lifetime (%s): %d guarded open / close / unmap / resume cycles, no consumer left\n", real_audio() ? "real output" : "silent sink", CYCLES);
|
||||
return 0;
|
||||
}
|
||||
int main(void) {
|
||||
if (test_surround_output()) return 1;
|
||||
if (test_surround_output() || test_output_lifetime()) return 1;
|
||||
char out[64], ev[64]; uint8_t prm[16]; gbuf b[3]; static int16_t pcm[3][4800 * 2];
|
||||
h_au_InitParam(prm);
|
||||
if (h_au_Open(out, ev, NULL, prm)) return 1;
|
||||
@@ -70,6 +101,7 @@ int main(void) {
|
||||
// Scenario 2: the real audio output sink (PCM all 0, inaudible) plays 6 blocks of 100 ms each, pausing at 150 ms (app to background / incoming call) and resuming at 350 ms.
|
||||
// Expected: during the pause the silent sink takes over and consumes in real time, all 6 blocks are released in order with 6 events; total duration ~ 600 ms (real time, neither stalled by the pause nor sped up).
|
||||
setenv("DM_AUDIO", "1", 1);
|
||||
if (test_output_lifetime()) return 1;
|
||||
char out2[64]; gbuf b2[6]; static int16_t pcm2[6][4800 * 2]; g_sig = 0;
|
||||
if (h_au_Open(out2, ev, NULL, prm)) return 1;
|
||||
for (int i = 0; i < 6; i++) { h_au_SetBuf(&b2[i], pcm2[i], sizeof pcm2[i], sizeof pcm2[i]); h_au_Append(out2, &b2[i]); }
|
||||
|
||||
Reference in new issue
Block a user