From 650905b73da9b29391106e99bdb5435ba072ef6f Mon Sep 17 00:00:00 2001 From: Dmitry Baryshkov Date: Tue, 19 Aug 2025 21:25:17 +0300 Subject: [PATCH 01/40] Remove CR-LF from the rest of the files Files under Git control should not be using CR-LF for EOL. Converting to native EOL symbols is handled by the git itself. Remove extra CR-LF terminators from several files that I missed in the commit 0b7e39fcbe3a ("src, inc: fix CRLF file endings") Fixes: 0b7e39fcbe3a ("src, inc: fix CRLF file endings") Signed-off-by: Dmitry Baryshkov --- Docs/apps_mem.md | 146 +++++------ test/README.md | 112 ++++---- test/fastrpc_test.c | 298 ++++++++++----------- test/qnn_run.md | 620 ++++++++++++++++++++++---------------------- 4 files changed, 588 insertions(+), 588 deletions(-) diff --git a/Docs/apps_mem.md b/Docs/apps_mem.md index 710b1c7..fbba76e 100644 --- a/Docs/apps_mem.md +++ b/Docs/apps_mem.md @@ -1,73 +1,73 @@ -# Design Document for apps_mem - -## Overview -apps_mem is an interface specifically designed to execute memory-related operations on APPS from DSP. These operations could include requests for memory allocation and mapping them on SMMU for DSP usage. The requests from DSP could be for either allocating & mapping a buffer or utilizing a file descriptor. Typically, the interface file is designed for communication from APPS to DSP. However, in this unique scenario, this interface file facilitates communication from DSP to APSS. - -## Interface Structure -module apps { - interface mem { - long request_map(in long heapid, in uint32 ion_flags, in uint32 rflags, in uint32 vin, in int32 len, rout uint32 vapps, rout uint32 vadsp); - long request_unmap(in uint32 vadsp, in int32 len); - long request_map64(in long heapid, in uint32 ion_flags, in uint32 rflags, in uint64 vin, in int64 len, rout uint64 vapps, rout uint64 vadsp); - long request_unmap64(in uint64 vadsp, in int64 len); - long share_map(in long fd, in long size, rout uint64 vapps, rout uint64 vadsp); - long share_unmap(in uint64 vadsp, in long size); - }; -}; - -This interface is compiled using the QAIC compiler. The generated stub/skel files are utilized on APPS and DSP. In this particular case, the generated skel file is linked to the APPS library and the stub is linked to DSP. The implementation file includes the implementation to allocate memory, map them on SMMU, and transmit it to DSP. The behavior of different APIs varies based on their implementation. - -## API Structure -```c -int apps_mem_request_map(int heapid, uint32 lflags, uint32 rflags, uint32 vin, int32 len, uint32* vapps, uint32* vadsp); -int apps_mem_request_map64(int heapid, uint32 ion_flags, uint32 rflags, uint64 vin, int64 len, uint64* vapps, uint64* vadsp); -int apps_mem_request_unmap(uint32 vadsp, int32 len); -int apps_mem_request_unmap64(uint64 vadsp, int64 len); -``` - -The apps_mem_request map and unmap APIs (both 32bit and 64bit) enable DSP to send heapid (ION heap id - in the case of ION heaps), lflags containing flags related to the heap, and rflags containing information related to the remote usage of this memory (for example, the memory is used for remote heap or dynamic loading or it should be a file descriptor or memory should be allocated from LLC). vin contains the virtual address of the memory on APPS, len is the total size of the memory allocated. vapps and vadsp are return values from kernel and DSP, which contains application space virtual address and DSP virtual address. - -```c -int apps_mem_share_map(int fd, int size, uint64* vapps, uint64* vadsp); -int apps_mem_share_unmap(uint64 vadsp, int size); -``` - -The apps_mem_share APIs are an extension to the request APIs described above. These APIs enable DSP to work on file descriptors instead of an address to memory. - -```c -int apps_mem_dma_handle_map(int fd, int offset, int size); -int apps_mem_dma_handle_unmap(int fd, int size); -``` - -The apps_mem_dma_handle APIs are an evolution from apps_mem_share APIs, where there is no need for the virtual address for DSP and APPS, and they completely rely on the file descriptor. - -## Remote flags supported -DSP supports five distinct flags, with some overlapping between them. Their primary function is to distinguish how this memory is utilized on the DSP. - -### ADSP_MMAP_REMOTE_HEAP_ADDR -This specific flag represents a request for memory by the Audio PD (Specifically) on the DSP for heap operations. When this flag is used, it’s assumed that the memory allocation takes place in the kernel space, not in the user space. The allocated memory is protected at stage 2 using the SMMU, to prevent any other masters from accessing this memory, with the exception of the DSP. - -### ADSP_MMAP_HEAP_ADDR -This flag is identical to the previously mentioned one, with the only difference being that it does not provide stage 2 protection at SMMU. - -### ADSP_MMAP_ADD_PAGES -ADD_PAGES indicates a memory request by a fastRPC signed/unsigned PD on the DSP for heap operations. Typically, memory is allocated in the user space for unsigned PD, while for signed PD, the allocation takes place in the kernel space. - -### ADSP_MMAP_ADD_PAGES_LLC -This flag enables clients to primarily allocate memory from the system’s L3 (LLC) as it offers low latency memory. Additionally, it allows clients to have memory accessible even in low power mode. This option is available only for priviledged applications. - -### FASTRPC_ALLOC_HLOS_FD -This flag primarily used in HAP APIs, which are exposed in the Hexagon SDK. Their main function is to allocate memory from the HLOS for different use cases of the clients. - -## Application -These APIs are typically used in scenarios where additional memory is required on DSP. For instance, the heap on DSP uses these APIs to bring additional memory from HLOS when the available memory on DSP heap hits a low threshold. In some cases, clients can also use these APIs to bring memory for their use case, but with the latest implementation, fastrpc_mmap is introduced thereby reducing the usage of these interfaces. - -## Sequence diagram - -### Map - -![Map sequence](images/apps_mem_map.png) - -### Unmap - -![UnMap sequence](images/apps_mem_unmap.png) +# Design Document for apps_mem + +## Overview +apps_mem is an interface specifically designed to execute memory-related operations on APPS from DSP. These operations could include requests for memory allocation and mapping them on SMMU for DSP usage. The requests from DSP could be for either allocating & mapping a buffer or utilizing a file descriptor. Typically, the interface file is designed for communication from APPS to DSP. However, in this unique scenario, this interface file facilitates communication from DSP to APSS. + +## Interface Structure +module apps { + interface mem { + long request_map(in long heapid, in uint32 ion_flags, in uint32 rflags, in uint32 vin, in int32 len, rout uint32 vapps, rout uint32 vadsp); + long request_unmap(in uint32 vadsp, in int32 len); + long request_map64(in long heapid, in uint32 ion_flags, in uint32 rflags, in uint64 vin, in int64 len, rout uint64 vapps, rout uint64 vadsp); + long request_unmap64(in uint64 vadsp, in int64 len); + long share_map(in long fd, in long size, rout uint64 vapps, rout uint64 vadsp); + long share_unmap(in uint64 vadsp, in long size); + }; +}; + +This interface is compiled using the QAIC compiler. The generated stub/skel files are utilized on APPS and DSP. In this particular case, the generated skel file is linked to the APPS library and the stub is linked to DSP. The implementation file includes the implementation to allocate memory, map them on SMMU, and transmit it to DSP. The behavior of different APIs varies based on their implementation. + +## API Structure +```c +int apps_mem_request_map(int heapid, uint32 lflags, uint32 rflags, uint32 vin, int32 len, uint32* vapps, uint32* vadsp); +int apps_mem_request_map64(int heapid, uint32 ion_flags, uint32 rflags, uint64 vin, int64 len, uint64* vapps, uint64* vadsp); +int apps_mem_request_unmap(uint32 vadsp, int32 len); +int apps_mem_request_unmap64(uint64 vadsp, int64 len); +``` + +The apps_mem_request map and unmap APIs (both 32bit and 64bit) enable DSP to send heapid (ION heap id - in the case of ION heaps), lflags containing flags related to the heap, and rflags containing information related to the remote usage of this memory (for example, the memory is used for remote heap or dynamic loading or it should be a file descriptor or memory should be allocated from LLC). vin contains the virtual address of the memory on APPS, len is the total size of the memory allocated. vapps and vadsp are return values from kernel and DSP, which contains application space virtual address and DSP virtual address. + +```c +int apps_mem_share_map(int fd, int size, uint64* vapps, uint64* vadsp); +int apps_mem_share_unmap(uint64 vadsp, int size); +``` + +The apps_mem_share APIs are an extension to the request APIs described above. These APIs enable DSP to work on file descriptors instead of an address to memory. + +```c +int apps_mem_dma_handle_map(int fd, int offset, int size); +int apps_mem_dma_handle_unmap(int fd, int size); +``` + +The apps_mem_dma_handle APIs are an evolution from apps_mem_share APIs, where there is no need for the virtual address for DSP and APPS, and they completely rely on the file descriptor. + +## Remote flags supported +DSP supports five distinct flags, with some overlapping between them. Their primary function is to distinguish how this memory is utilized on the DSP. + +### ADSP_MMAP_REMOTE_HEAP_ADDR +This specific flag represents a request for memory by the Audio PD (Specifically) on the DSP for heap operations. When this flag is used, it’s assumed that the memory allocation takes place in the kernel space, not in the user space. The allocated memory is protected at stage 2 using the SMMU, to prevent any other masters from accessing this memory, with the exception of the DSP. + +### ADSP_MMAP_HEAP_ADDR +This flag is identical to the previously mentioned one, with the only difference being that it does not provide stage 2 protection at SMMU. + +### ADSP_MMAP_ADD_PAGES +ADD_PAGES indicates a memory request by a fastRPC signed/unsigned PD on the DSP for heap operations. Typically, memory is allocated in the user space for unsigned PD, while for signed PD, the allocation takes place in the kernel space. + +### ADSP_MMAP_ADD_PAGES_LLC +This flag enables clients to primarily allocate memory from the system’s L3 (LLC) as it offers low latency memory. Additionally, it allows clients to have memory accessible even in low power mode. This option is available only for priviledged applications. + +### FASTRPC_ALLOC_HLOS_FD +This flag primarily used in HAP APIs, which are exposed in the Hexagon SDK. Their main function is to allocate memory from the HLOS for different use cases of the clients. + +## Application +These APIs are typically used in scenarios where additional memory is required on DSP. For instance, the heap on DSP uses these APIs to bring additional memory from HLOS when the available memory on DSP heap hits a low threshold. In some cases, clients can also use these APIs to bring memory for their use case, but with the latest implementation, fastrpc_mmap is introduced thereby reducing the usage of these interfaces. + +## Sequence diagram + +### Map + +![Map sequence](images/apps_mem_map.png) + +### Unmap + +![UnMap sequence](images/apps_mem_unmap.png) diff --git a/test/README.md b/test/README.md index dbe045e..9a5c640 100644 --- a/test/README.md +++ b/test/README.md @@ -1,57 +1,57 @@ -# fastrpc_test - -This folder contains a test application (`fastrpc_test.c`) that demonstrates the usage of FastRPC (Fast Remote Procedure Call) to offload computations to different DSP (Digital Signal Processor) domains. The test application supports multiple examples, including a simple calculator service, a HAP example, and a multithreading example. - -## Pushing Files to the Device - -After building the application, the following files and directories need to be pushed to the device: - -1. **fastrpc_test Binary**: The compiled test application. -2. **android Directory**: Contains shared libraries for the Android platform. -3. **linux Directory**: Contains shared libraries for the Linux platform. -4. **v68 Directory**: Contains skeletons for the v68 architecture version. -5. **v75 Directory**: Contains skeletons for the v75 architecture version. - -Copy the following files and directories to `/usr/bin` on the target device: - -- `/path/to/your/fastrpc/test/.libs/fastrpc_test` -- `/path/to/your/fastrpc/test/android` (if using Android) -- `/path/to/your/fastrpc/test/linux` (if using Linux) -- `/path/to/your/fastrpc/test/v68` -- `/path/to/your/fastrpc/test/v75` - -**Note**: Push the `android` directory if you are using the Android platform, and the `linux` directory if you are using the Linux platform. - -## Running the Test - -To run the test application, follow these steps: - -1. Navigate to the `/usr/bin` directory on the device. -2. Execute the `fastrpc_test` binary with the appropriate options. - -Example command: - -```bash -cd /usr/bin -./fastrpc_test -d 3 -U 1 -t linux -a v68 -``` - -### Options - -- `-d domain`: Run on a specific domain. - - `0`: Run the example on ADSP - - `1`: Run the example on MDSP - - `2`: Run the example on SDSP - - `3`: Run the example on CDSP - - **Default Value**: `3` (CDSP) for targets having CDSP. - -- `-U unsigned_PD`: Run on signed or unsigned PD. - - `0`: Run on signed PD. - - `1`: Run on unsigned PD. - - **Default Value**: `1` - -- `-t target`: Specify the target platform (android or linux). - - **Default Value**: `linux` - -- `-a arch_version`: Specify the architecture version (e.g., v68, v75). +# fastrpc_test + +This folder contains a test application (`fastrpc_test.c`) that demonstrates the usage of FastRPC (Fast Remote Procedure Call) to offload computations to different DSP (Digital Signal Processor) domains. The test application supports multiple examples, including a simple calculator service, a HAP example, and a multithreading example. + +## Pushing Files to the Device + +After building the application, the following files and directories need to be pushed to the device: + +1. **fastrpc_test Binary**: The compiled test application. +2. **android Directory**: Contains shared libraries for the Android platform. +3. **linux Directory**: Contains shared libraries for the Linux platform. +4. **v68 Directory**: Contains skeletons for the v68 architecture version. +5. **v75 Directory**: Contains skeletons for the v75 architecture version. + +Copy the following files and directories to `/usr/bin` on the target device: + +- `/path/to/your/fastrpc/test/.libs/fastrpc_test` +- `/path/to/your/fastrpc/test/android` (if using Android) +- `/path/to/your/fastrpc/test/linux` (if using Linux) +- `/path/to/your/fastrpc/test/v68` +- `/path/to/your/fastrpc/test/v75` + +**Note**: Push the `android` directory if you are using the Android platform, and the `linux` directory if you are using the Linux platform. + +## Running the Test + +To run the test application, follow these steps: + +1. Navigate to the `/usr/bin` directory on the device. +2. Execute the `fastrpc_test` binary with the appropriate options. + +Example command: + +```bash +cd /usr/bin +./fastrpc_test -d 3 -U 1 -t linux -a v68 +``` + +### Options + +- `-d domain`: Run on a specific domain. + - `0`: Run the example on ADSP + - `1`: Run the example on MDSP + - `2`: Run the example on SDSP + - `3`: Run the example on CDSP + - **Default Value**: `3` (CDSP) for targets having CDSP. + +- `-U unsigned_PD`: Run on signed or unsigned PD. + - `0`: Run on signed PD. + - `1`: Run on unsigned PD. + - **Default Value**: `1` + +- `-t target`: Specify the target platform (android or linux). + - **Default Value**: `linux` + +- `-a arch_version`: Specify the architecture version (e.g., v68, v75). - **Default Value**: `v68` \ No newline at end of file diff --git a/test/fastrpc_test.c b/test/fastrpc_test.c index 9edd8ab..275bcc1 100644 --- a/test/fastrpc_test.c +++ b/test/fastrpc_test.c @@ -1,150 +1,150 @@ -// Copyright (c) 2024, Qualcomm Innovation Center, Inc. All rights reserved. -// SPDX-License-Identifier: BSD-3-Clause - -#include -#include -#include -#include -#include -#include -#include -#include -#include // For PATH_MAX - -typedef int (*run_test_t)(int domain_id, bool is_unsignedpd_enabled); - -static void print_usage() { - printf("Usage:\n" - " fastrpc_test [-d domain] [-U unsigned_PD] [-t target] [-a arch_version]\n\n" - "Options:\n" - "-d domain: Run on a specific domain.\n" - " 0: Run the example on ADSP\n" - " 1: Run the example on MDSP\n" - " 2: Run the example on SDSP\n" - " 3: Run the example on CDSP\n" - " Default Value: 3(CDSP) for targets having CDSP.\n" - "-U unsigned_PD: Run on signed or unsigned PD.\n" - " 0: Run on signed PD.\n" - " 1: Run on unsigned PD.\n" - " Default Value: 1\n" - "-t target: Specify the target platform (android or linux).\n" - " Default Value: linux\n" - "-a arch_version: Specify the architecture version (v68 or v75).\n" - " Default Value: v68\n" - ); -} - -int main(int argc, char *argv[]) { - int domain_id = 3; // Default domain ID for CDSP - bool is_unsignedpd_enabled = true; // Default to unsigned PD - const char *target = "linux"; // Default target platform - const char *arch_version = "v68"; // Default architecture version - char abs_lib_path[PATH_MAX]; - char ld_lib_path[PATH_MAX]; - char dsp_lib_path[PATH_MAX]; - DIR *dir; - struct dirent *entry; - char full_lib_path[PATH_MAX]; - void *lib_handle = NULL; - run_test_t run_test = NULL; - int nErr = 0; - - int opt; - while ((opt = getopt(argc, argv, "d:U:t:a:")) != -1) { - switch (opt) { - case 'd': - domain_id = atoi(optarg); - break; - case 'U': - is_unsignedpd_enabled = atoi(optarg) != 0; - break; - case 't': - target = optarg; - break; - case 'a': - arch_version = optarg; - if (strcmp(arch_version, "v68") != 0 && strcmp(arch_version, "v75") != 0) { - printf("\nERROR: Invalid architecture version (-a). Must be v68 or v75.\n"); - print_usage(); - return -1; - } - break; - default: - print_usage(); - return -1; - } - } - - // Construct the absolute library path - snprintf(abs_lib_path, sizeof(abs_lib_path), "%s", target); - - if (realpath(abs_lib_path, abs_lib_path) == NULL) { - fprintf(stderr, "Error resolving path %s: %s\n", abs_lib_path, strerror(errno)); - return -1; - } - - // Construct the absolute DSP library path - snprintf(dsp_lib_path, sizeof(dsp_lib_path), "%s", arch_version); - - if (realpath(dsp_lib_path, dsp_lib_path) == NULL) { - fprintf(stderr, "Error resolving path %s: %s\n", dsp_lib_path, strerror(errno)); - return -1; - } - - // Construct LD_LIBRARY_PATH and DSP_LIBRARY_PATH - snprintf(ld_lib_path, sizeof(ld_lib_path), "%s", abs_lib_path); - - if (setenv("LD_LIBRARY_PATH", ld_lib_path, 1) != 0) { - fprintf(stderr, "Error setting LD_LIBRARY_PATH: %s\n", strerror(errno)); - return -1; - } - - if (setenv("DSP_LIBRARY_PATH", dsp_lib_path, 1) != 0) { - fprintf(stderr, "Error setting DSP_LIBRARY_PATH: %s\n", strerror(errno)); - return -1; - } - - dir = opendir(abs_lib_path); - if (!dir) { - fprintf(stderr, "Error opening directory %s: %s\n", abs_lib_path, strerror(errno)); - return -1; - } - - while ((entry = readdir(dir)) != NULL) { - if (entry->d_type == DT_REG && strstr(entry->d_name, ".so")) { - snprintf(full_lib_path, sizeof(full_lib_path), "%s/%s", abs_lib_path, entry->d_name); - - lib_handle = dlopen(full_lib_path, RTLD_LAZY); - if (!lib_handle) { - fprintf(stderr, "Error loading %s: %s\n", full_lib_path, dlerror()); - continue; - } - - run_test = (run_test_t)dlsym(lib_handle, "run_test"); - if (!run_test) { - fprintf(stderr, "Symbol 'run_test' not found in %s\n", full_lib_path); - dlclose(lib_handle); - continue; - } - - nErr = run_test(domain_id, is_unsignedpd_enabled); - if (nErr != 0) { - printf("Test failed with error code 0x%x in %s\n", nErr, full_lib_path); - } else { - printf("Success in %s\n", full_lib_path); - } - - dlclose(lib_handle); - } - } - - closedir(dir); - - if (nErr != 0) { - printf("Test failed with error code 0x%x\n", nErr); - } else { - printf("All tests completed successfully\n"); - } - - return nErr; +// Copyright (c) 2024, Qualcomm Innovation Center, Inc. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include +#include +#include +#include +#include // For PATH_MAX + +typedef int (*run_test_t)(int domain_id, bool is_unsignedpd_enabled); + +static void print_usage() { + printf("Usage:\n" + " fastrpc_test [-d domain] [-U unsigned_PD] [-t target] [-a arch_version]\n\n" + "Options:\n" + "-d domain: Run on a specific domain.\n" + " 0: Run the example on ADSP\n" + " 1: Run the example on MDSP\n" + " 2: Run the example on SDSP\n" + " 3: Run the example on CDSP\n" + " Default Value: 3(CDSP) for targets having CDSP.\n" + "-U unsigned_PD: Run on signed or unsigned PD.\n" + " 0: Run on signed PD.\n" + " 1: Run on unsigned PD.\n" + " Default Value: 1\n" + "-t target: Specify the target platform (android or linux).\n" + " Default Value: linux\n" + "-a arch_version: Specify the architecture version (v68 or v75).\n" + " Default Value: v68\n" + ); +} + +int main(int argc, char *argv[]) { + int domain_id = 3; // Default domain ID for CDSP + bool is_unsignedpd_enabled = true; // Default to unsigned PD + const char *target = "linux"; // Default target platform + const char *arch_version = "v68"; // Default architecture version + char abs_lib_path[PATH_MAX]; + char ld_lib_path[PATH_MAX]; + char dsp_lib_path[PATH_MAX]; + DIR *dir; + struct dirent *entry; + char full_lib_path[PATH_MAX]; + void *lib_handle = NULL; + run_test_t run_test = NULL; + int nErr = 0; + + int opt; + while ((opt = getopt(argc, argv, "d:U:t:a:")) != -1) { + switch (opt) { + case 'd': + domain_id = atoi(optarg); + break; + case 'U': + is_unsignedpd_enabled = atoi(optarg) != 0; + break; + case 't': + target = optarg; + break; + case 'a': + arch_version = optarg; + if (strcmp(arch_version, "v68") != 0 && strcmp(arch_version, "v75") != 0) { + printf("\nERROR: Invalid architecture version (-a). Must be v68 or v75.\n"); + print_usage(); + return -1; + } + break; + default: + print_usage(); + return -1; + } + } + + // Construct the absolute library path + snprintf(abs_lib_path, sizeof(abs_lib_path), "%s", target); + + if (realpath(abs_lib_path, abs_lib_path) == NULL) { + fprintf(stderr, "Error resolving path %s: %s\n", abs_lib_path, strerror(errno)); + return -1; + } + + // Construct the absolute DSP library path + snprintf(dsp_lib_path, sizeof(dsp_lib_path), "%s", arch_version); + + if (realpath(dsp_lib_path, dsp_lib_path) == NULL) { + fprintf(stderr, "Error resolving path %s: %s\n", dsp_lib_path, strerror(errno)); + return -1; + } + + // Construct LD_LIBRARY_PATH and DSP_LIBRARY_PATH + snprintf(ld_lib_path, sizeof(ld_lib_path), "%s", abs_lib_path); + + if (setenv("LD_LIBRARY_PATH", ld_lib_path, 1) != 0) { + fprintf(stderr, "Error setting LD_LIBRARY_PATH: %s\n", strerror(errno)); + return -1; + } + + if (setenv("DSP_LIBRARY_PATH", dsp_lib_path, 1) != 0) { + fprintf(stderr, "Error setting DSP_LIBRARY_PATH: %s\n", strerror(errno)); + return -1; + } + + dir = opendir(abs_lib_path); + if (!dir) { + fprintf(stderr, "Error opening directory %s: %s\n", abs_lib_path, strerror(errno)); + return -1; + } + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_type == DT_REG && strstr(entry->d_name, ".so")) { + snprintf(full_lib_path, sizeof(full_lib_path), "%s/%s", abs_lib_path, entry->d_name); + + lib_handle = dlopen(full_lib_path, RTLD_LAZY); + if (!lib_handle) { + fprintf(stderr, "Error loading %s: %s\n", full_lib_path, dlerror()); + continue; + } + + run_test = (run_test_t)dlsym(lib_handle, "run_test"); + if (!run_test) { + fprintf(stderr, "Symbol 'run_test' not found in %s\n", full_lib_path); + dlclose(lib_handle); + continue; + } + + nErr = run_test(domain_id, is_unsignedpd_enabled); + if (nErr != 0) { + printf("Test failed with error code 0x%x in %s\n", nErr, full_lib_path); + } else { + printf("Success in %s\n", full_lib_path); + } + + dlclose(lib_handle); + } + } + + closedir(dir); + + if (nErr != 0) { + printf("Test failed with error code 0x%x\n", nErr); + } else { + printf("All tests completed successfully\n"); + } + + return nErr; } \ No newline at end of file diff --git a/test/qnn_run.md b/test/qnn_run.md index 48bd241..a1fbe31 100644 --- a/test/qnn_run.md +++ b/test/qnn_run.md @@ -1,311 +1,311 @@ -# Document for Running QNN Models - -## For Linux Host (Setup) - -### Step 1: Install Qualcomm AI Engine Direct (QNN SDK) - -1. Download the QNN SDK: - * Go to the QNN SDK product [page](https://www.qualcomm.com/developer/software/qualcomm-ai-engine-direct-sdk). - * Click **Get Software** to download the QNN SDK. - * Unzip the downloaded SDK. -2. Set Up Your Environment: - * Open a terminal. - * Navigate to `qairt/` inside the unzipped QNN SDK. - * Run the following commands to set up the environment: - + `cd bin` - + `source ./envsetup.sh` - * This will automatically set the environment variables `QNN_SDK_ROOT` and `SNPE_ROOT`. - * To save this for future bash terminals, run: - + `echo 'export QNN_SDK_ROOT="${QNN_SDK_ROOT}"' >> ~/.bashrc` -3. Check Dependencies: - * Run: - + `sudo ${QNN_SDK_ROOT}/bin/check-linux-dependency.sh` - * When all necessary dependencies are installed, the script will display “Done!!”. -4. Verify Toolchain Installation: - * Run: - + `${QNN_SDK_ROOT}/bin/envcheck -c` - * This will verify that the required toolchain is installed successfully. - -### Step 2: Install QNN SDK Dependencies - -1. Install Python 3.10: - * Run the following commands: - + `sudo apt-get update && sudo apt-get install python3.10 python3-distutils libpython3.10` - * Verify the installation by running: - + `python3 --version` -2. Navigate to QNN SDK Root: - * Run: - + `cd ${QNN_SDK_ROOT}` -3. Create and Activate a Virtual Environment: - * Run the following commands (you may rename `MY_ENV_NAME` to any name you prefer): - + `python3 -m venv MY_ENV_NAME` - + `source MY_ENV_NAME/bin/activate` -4. Update Dependencies: - * Run: - + `python "${QNN_SDK_ROOT}/bin/check-python-dependency"` - -### Step 3: Install Model Frameworks - -1. Identify Relevant Frameworks: - * QNN supports multiple model frameworks. Install only the ones relevant for the AI model files you want to use. - * **Warning:** You do not need to install all packages listed here. -2. Note: - * You can install a package by running: - + `pip3 install package==version` - * Example Installations: - + `pip3 install torch==1.13.1` - + `pip3 install tensorflow==2.10.1` - + `pip3 install tflite==2.3.0` - + `pip3 install torchvision==0.14.1` - + `pip3 install onnx==1.12.0` - + `pip3 install onnxruntime==1.17.1` - + `pip3 install onnxsim==0.4.36` - -### Step 4: Install Target Device OS-Specific Toolchain Code - -#### Working with an Android Target Device: - -1. Install Android NDK: - * Download the [Android NDK r26c](https://dl.google.com/android/repository/android-ndk-r26c-linux.zip). - * Unzip the file. -2. Set Up Environment Variables: - * Open a terminal. - * Replace `` with the path to the unzipped `android-ndk-r26c` folder, then run: - + `echo 'export ANDROID_NDK_ROOT=""' >> ~/.bashrc` - * Add the location of the unzipped file to your PATH by running: - + `echo 'export PATH="${ANDROID_NDK_ROOT}:${PATH}"' >> ~/.bashrc` - + `source ~/.bashrc` -3. Verify Environment Variables: - * Run: - + `${QNN_SDK_ROOT}/bin/envcheck -n` - -#### Working with a Linux Target Device: - -1. Verify Clang++14 Installation: - * For Linux target devices, you will likely need to use clang++14 to build models for them using the QNN SDK. - * You can verify if you have clang++14 by running: - + `${QNN_SDK_ROOT}/bin/envcheck -c` -2. Set Up Cross Compiler (for **Linux Embedded**): - * Open a new terminal - * Follow these steps to set up the cross compiler: - + `wget https://artifacts.codelinaro.org/artifactory/qli-ci/flashable-binaries/qimpsdk/qcm6490/x86/qcom-6.6.28-QLI.1.1-Ver.1.1_qim-product-sdk-1.1.3.zip` - + `unzip qcom-6.6.28-QLI.1.1-Ver.1.1_qim-product-sdk-1.1.3.zip` - + `umask a+rx` - + `cd /path/to/unzip/target/qcm6490/sdk` - + `sh qcom-wayland-x86_64-qcom-multimedia-image-armv8-2a-qcm6490-toolchain-ext-1.0.sh` - + `export ESDK_ROOT=` - + `cd $ESDK_ROOT` - + `source environment-setup-armv8-2a-qcom-linux` - -## CNN to QNN for Linux Host - -### Step 1: Set Up The Example TensorFlow Model - -1. Verify TensorFlow Installation: - * Run: - + `python3 -m pip show tensorflow` -2. Set the TENSORFLOW_HOME Environment Variable: - * Run the following commands: - ``` - tensorflowLocation=$(python -m pip show tensorflow | grep '^Location: ' | awk '{print $2}') - export TENSORFLOW_HOME="$tensorflowLocation/tensorflow" - echo "export TENSORFLOW_HOME=$tensorflowLocation/tensorflow" >> ~/.bashrc - ``` -3. Verify Environment Variable: - * Run: - + `${TENSORFLOW_HOME}` -4. Create Test Data: - * Run: - + `python3 ${QNN_SDK_ROOT}/examples/Models/InceptionV3/scripts/setup_inceptionv3.py -a ~/tmpdir -d` - * This will create the test data for this tutorial: - + Model file at: `${QNN_SDK_ROOT}/examples/Models/InceptionV3/tensorflow/inception_v3_2016_08_28_frozen.pb` - + Raw images at: `${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped` - -### Step 2: Convert the CNN Model into a QNN Model - -1. Quantized Model Conversion: - * To use a quantized model, pass in the `--input_list` flag to specify the input. - * Run the following command: - ``` - python ${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-tensorflow-converter \ - --input_network "${QNN_SDK_ROOT}/examples/Models/InceptionV3/tensorflow/inception_v3_2016_08_28_frozen.pb" \ - --input_dim input 1,299,299,3 \ - --input_list "${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/raw_list.txt" \ - --out_node "InceptionV3/Predictions/Reshape_1" \ - --output_path "${QNN_SDK_ROOT}/examples/Models/InceptionV3/model/Inception_v3.cpp" - ``` -2. Flags Explanation: - * `--input_network`: Path to the source framework model. - * `--input_dim`: Input name followed by dimensions. - * `--input_list`: Absolute path to input data (required for quantization). - * `--out_node`: Name of the graph’s output Tensor Names. - * `--output_path`: Indicates where to put the output files. -3. Artifacts Produced: - * `Inception_v3.cpp`: Contains the sequence of API calls. - * `Inception_v3.bin`: Static data associated with the model. - * `Inception_v3_net.json`: Describes the model data in a standardized manner (not needed to build the QNN model later on). -4. Location of Artifacts: - * You can find the artifacts in the `${QNN_SDK_ROOT}/examples/Models/InceptionV3/model` directory. - -### Other Model Types: -If you are using another type of model, you can refer to the [Tools page](https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/tools.html) for a table of potential scripts to help convert them into QNN format. These scripts will have a similar qnn-model-type-converter naming convention. - -### Step 3: Model Build on Linux Host - -1. Choose Target Architecture: - * Select the most relevant supported target architecture from the following list: - + 64-bit Linux targets: `x86_64-linux-clang` - + 64-bit Android devices: `aarch64-android` - + Qualcomm’s QNX OS: `aarch64-qnx` (Note: This architecture is not supported by default in the QNN SDK.) - + OpenEmbedded Linux (GCC 11.2): `aarch64-oe-linux-gcc11.2` - + OpenEmbedded Linux (GCC 9.3): `aarch64-oe-linux-gcc9.3` - + OpenEmbedded Linux (GCC 8.2): `aarch64-oe-linux-gcc8.2` - + Ubuntu Linux (GCC 9.4): `aarch64-ubuntu-gcc9.4` - + Ubuntu Linux (GCC 7.5): `aarch64-ubuntu-gcc7.5` -2. Set Target Architecture: - * On your host machine, set the target architecture of your target device by running: - + `export QNN_TARGET_ARCH="your-target-architecture-from-above"` - * For **Android**: - + `export QNN_TARGET_ARCH="aarch64-android"` - * For **Linux Embedded**: - + `export QNN_TARGET_ARCH="aarch64-oe-linux-gcc11.2"` -3. Generate Model Library: - * Run the following command on your host machine: - ``` - python3 "${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-model-lib-generator" \ - -c "${QNN_SDK_ROOT}/examples/Models/InceptionV3/model/Inception_v3.cpp" \ - -b "${QNN_SDK_ROOT}/examples/Models/InceptionV3/model/Inception_v3.bin" \ - -o "${QNN_SDK_ROOT}/examples/Models/InceptionV3/model_libs" \ - -t ${QNN_TARGET_ARCH} - ``` - * `-c`: Path to the `.cpp` QNN model file. - * `-b`: Path to the `.bin` QNN model file (optional but recommended). - * `-o`: Path to the output folder. - * `-t`: Target architecture to build for. -4. Verify Output File: - * Run: - + `ls ${QNN_SDK_ROOT}/examples/Models/InceptionV3/model_libs/${QNN_TARGET_ARCH}` - * Verify that the output file `libInception_v3.so` is inside. This file will be used on the target device to execute inferences. The output `.so` file will be located in the `model_libs` directory, named according to the target architecture. - -### Step 4: Model Build on Linux Host - -Users can set custom options and different performance modes to the HTP Backend through the backend config. Follow these steps to create the necessary JSON files: - -1. Create Backend Config File (`be.json`): - * For **Android** with the following content: - ```json - { - "graphs": [ - { - "graph_names": [ - "Inception_v3" - ], - "vtcm_mb": 8 - } - ], - "devices": [ - { - "htp_arch": "v79" - } - ] - } - ``` - * For **Linux Embedded** with the following content: - ```json - { - "graphs": [ - { - "graph_names": [ - "Inception_v3" - ], - "vtcm_mb": 8 - } - ], - "devices": [ - { - "htp_arch": "v73" - } - ] - } - ``` -2. Create Backend Extensions Config File (`config.json`): - * The config file with minimum parameters specified through JSON is given below: - ```json - { - "backend_extensions": { - "shared_library_path": "./libQnnHtpNetRunExtensions.so", - "config_file_path": "./be.json" - } - } - ``` -3. Place JSON Files in the Model Libraries Path: - * Ensure that the `be.json` and `config.json` files are located in the following path: - + `${QNN_SDK_ROOT}/examples/Models/InceptionV3/model_libs/${QNN_TARGET_ARCH}` - -### Step 5: Prepare the Target Device - -1. Open a Terminal on the Target Device: - * Run the following commands to create a destination folder and set the necessary permissions: - ``` - adb shell "mkdir -p /data/local/tmp" - adb shell "ln -s /etc/ /data/local/tmp" - adb shell "chmod -R 777 /data/local/tmp" - adb shell "mkdir -p /data/local/tmp/inception_v3" - ``` -2. Push Necessary Files: - * Push `libQnnHtp.so` and other necessary executables, input data, and input list from your host machine to `/data/local/tmp/inception_v3` on the target device: - ``` - adb push \path\to\qairt\2.32.0.250228\lib\${QNN_TARGET_ARCH} /data/local/tmp/inception_v3 - adb shell "cd /data/local/tmp/inception_v3; mv ./${QNN_TARGET_ARCH}/* ./; rmdir ${QNN_TARGET_ARCH}" - ``` - * For **Android**: - + `adb push \path\to\qairt\2.32.0.250228\lib\hexagon-v79\unsigned /data/local/tmp/inception_v3` - * For **Linux Embedded**: - + `adb push \path\to\qairt\2.32.0.250228\lib\hexagon-v73\unsigned /data/local/tmp/inception_v3` - - * Continue remaining steps on the target device: - ``` - adb shell "cd /data/local/tmp/inception_v3; mv ./unsigned/* ./; rmdir unsigned" - adb push \path\to\qairt\2.32.0.250228\examples\Models\InceptionV3\model_libs\${QNN_TARGET_ARCH}\ /data/local/tmp/inception_v3 - adb shell "cd /data/local/tmp/inception_v3; mv ./${QNN_TARGET_ARCH}/* ./; rmdir ${QNN_TARGET_ARCH}" - ``` - ``` - adb push \path\to\qairt\2.32.0.250228\examples\Models\InceptionV3\data\cropped /data/local/tmp/inception_v3 - adb push \path\to\qairt\2.32.0.250228\examples\Models\InceptionV3\data\target_raw_list.txt /data/local/tmp/inception_v3 - adb push \path\to\qairt\2.32.0.250228\bin\${QNN_TARGET_ARCH}\qnn-net-run /data/local/tmp/inception_v3 - adb shell "chmod 777 /data/local/tmp/inception_v3/qnn-net-run" - ``` -3. Run the Model & Pull Output Directory to Host Machine: - * On the target device, run the following command to execute the model: - ``` - adb shell "LD_LIBRARY_PATH=/data/local/tmp/inception_v3 DSP_LIBRARY_PATH=/data/local/tmp/inception_v3; cd /data/local/tmp/inception_v3; ./qnn-net-run --model ./libInception_v3.so --input_list ./target_raw_list.txt --backend ./libQnnHtp.so --output_dir ./output --config_file ./config.json" - ``` - * Run the following command to pull the output directory from the target device to your host machine: - + `adb pull /data/local/tmp/inception_v3/output \path\to\qairt\2.32.0.250228\examples\Models\InceptionV3` - -### Step 6: View Classification Result - -1. Navigate to the Example Directory on Host Machine: - * Run: - + `cd ${QNN_SDK_ROOT}/examples/Models/InceptionV3` -2. Run the Script to View Classification Results: - * Execute the following command to view the classification results: - ``` - python3 "./scripts/show_inceptionv3_classifications.py" \ - -i "./data/cropped/raw_list.txt" \ - -o "./output" \ - -l "./data/imagenet_slim_labels.txt" - ``` - * Ensure that the classification results in the output match the following: - ``` - ${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/notice_sign.raw 0.152344 459 brass - ${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/chairs.raw 0.281250 832 studio couch - ${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/trash_bin.raw 0.800781 413 ashcan - ${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/plastic_cup.raw 0.988281 648 measuring cup - ``` - -### References - -* [Linux Setup](https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/linux_setup.html) -* [CNN to QNN for Linux Host](https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/qnn_tutorial_linux_host.html) +# Document for Running QNN Models + +## For Linux Host (Setup) + +### Step 1: Install Qualcomm AI Engine Direct (QNN SDK) + +1. Download the QNN SDK: + * Go to the QNN SDK product [page](https://www.qualcomm.com/developer/software/qualcomm-ai-engine-direct-sdk). + * Click **Get Software** to download the QNN SDK. + * Unzip the downloaded SDK. +2. Set Up Your Environment: + * Open a terminal. + * Navigate to `qairt/` inside the unzipped QNN SDK. + * Run the following commands to set up the environment: + + `cd bin` + + `source ./envsetup.sh` + * This will automatically set the environment variables `QNN_SDK_ROOT` and `SNPE_ROOT`. + * To save this for future bash terminals, run: + + `echo 'export QNN_SDK_ROOT="${QNN_SDK_ROOT}"' >> ~/.bashrc` +3. Check Dependencies: + * Run: + + `sudo ${QNN_SDK_ROOT}/bin/check-linux-dependency.sh` + * When all necessary dependencies are installed, the script will display “Done!!”. +4. Verify Toolchain Installation: + * Run: + + `${QNN_SDK_ROOT}/bin/envcheck -c` + * This will verify that the required toolchain is installed successfully. + +### Step 2: Install QNN SDK Dependencies + +1. Install Python 3.10: + * Run the following commands: + + `sudo apt-get update && sudo apt-get install python3.10 python3-distutils libpython3.10` + * Verify the installation by running: + + `python3 --version` +2. Navigate to QNN SDK Root: + * Run: + + `cd ${QNN_SDK_ROOT}` +3. Create and Activate a Virtual Environment: + * Run the following commands (you may rename `MY_ENV_NAME` to any name you prefer): + + `python3 -m venv MY_ENV_NAME` + + `source MY_ENV_NAME/bin/activate` +4. Update Dependencies: + * Run: + + `python "${QNN_SDK_ROOT}/bin/check-python-dependency"` + +### Step 3: Install Model Frameworks + +1. Identify Relevant Frameworks: + * QNN supports multiple model frameworks. Install only the ones relevant for the AI model files you want to use. + * **Warning:** You do not need to install all packages listed here. +2. Note: + * You can install a package by running: + + `pip3 install package==version` + * Example Installations: + + `pip3 install torch==1.13.1` + + `pip3 install tensorflow==2.10.1` + + `pip3 install tflite==2.3.0` + + `pip3 install torchvision==0.14.1` + + `pip3 install onnx==1.12.0` + + `pip3 install onnxruntime==1.17.1` + + `pip3 install onnxsim==0.4.36` + +### Step 4: Install Target Device OS-Specific Toolchain Code + +#### Working with an Android Target Device: + +1. Install Android NDK: + * Download the [Android NDK r26c](https://dl.google.com/android/repository/android-ndk-r26c-linux.zip). + * Unzip the file. +2. Set Up Environment Variables: + * Open a terminal. + * Replace `` with the path to the unzipped `android-ndk-r26c` folder, then run: + + `echo 'export ANDROID_NDK_ROOT=""' >> ~/.bashrc` + * Add the location of the unzipped file to your PATH by running: + + `echo 'export PATH="${ANDROID_NDK_ROOT}:${PATH}"' >> ~/.bashrc` + + `source ~/.bashrc` +3. Verify Environment Variables: + * Run: + + `${QNN_SDK_ROOT}/bin/envcheck -n` + +#### Working with a Linux Target Device: + +1. Verify Clang++14 Installation: + * For Linux target devices, you will likely need to use clang++14 to build models for them using the QNN SDK. + * You can verify if you have clang++14 by running: + + `${QNN_SDK_ROOT}/bin/envcheck -c` +2. Set Up Cross Compiler (for **Linux Embedded**): + * Open a new terminal + * Follow these steps to set up the cross compiler: + + `wget https://artifacts.codelinaro.org/artifactory/qli-ci/flashable-binaries/qimpsdk/qcm6490/x86/qcom-6.6.28-QLI.1.1-Ver.1.1_qim-product-sdk-1.1.3.zip` + + `unzip qcom-6.6.28-QLI.1.1-Ver.1.1_qim-product-sdk-1.1.3.zip` + + `umask a+rx` + + `cd /path/to/unzip/target/qcm6490/sdk` + + `sh qcom-wayland-x86_64-qcom-multimedia-image-armv8-2a-qcm6490-toolchain-ext-1.0.sh` + + `export ESDK_ROOT=` + + `cd $ESDK_ROOT` + + `source environment-setup-armv8-2a-qcom-linux` + +## CNN to QNN for Linux Host + +### Step 1: Set Up The Example TensorFlow Model + +1. Verify TensorFlow Installation: + * Run: + + `python3 -m pip show tensorflow` +2. Set the TENSORFLOW_HOME Environment Variable: + * Run the following commands: + ``` + tensorflowLocation=$(python -m pip show tensorflow | grep '^Location: ' | awk '{print $2}') + export TENSORFLOW_HOME="$tensorflowLocation/tensorflow" + echo "export TENSORFLOW_HOME=$tensorflowLocation/tensorflow" >> ~/.bashrc + ``` +3. Verify Environment Variable: + * Run: + + `${TENSORFLOW_HOME}` +4. Create Test Data: + * Run: + + `python3 ${QNN_SDK_ROOT}/examples/Models/InceptionV3/scripts/setup_inceptionv3.py -a ~/tmpdir -d` + * This will create the test data for this tutorial: + + Model file at: `${QNN_SDK_ROOT}/examples/Models/InceptionV3/tensorflow/inception_v3_2016_08_28_frozen.pb` + + Raw images at: `${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped` + +### Step 2: Convert the CNN Model into a QNN Model + +1. Quantized Model Conversion: + * To use a quantized model, pass in the `--input_list` flag to specify the input. + * Run the following command: + ``` + python ${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-tensorflow-converter \ + --input_network "${QNN_SDK_ROOT}/examples/Models/InceptionV3/tensorflow/inception_v3_2016_08_28_frozen.pb" \ + --input_dim input 1,299,299,3 \ + --input_list "${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/raw_list.txt" \ + --out_node "InceptionV3/Predictions/Reshape_1" \ + --output_path "${QNN_SDK_ROOT}/examples/Models/InceptionV3/model/Inception_v3.cpp" + ``` +2. Flags Explanation: + * `--input_network`: Path to the source framework model. + * `--input_dim`: Input name followed by dimensions. + * `--input_list`: Absolute path to input data (required for quantization). + * `--out_node`: Name of the graph’s output Tensor Names. + * `--output_path`: Indicates where to put the output files. +3. Artifacts Produced: + * `Inception_v3.cpp`: Contains the sequence of API calls. + * `Inception_v3.bin`: Static data associated with the model. + * `Inception_v3_net.json`: Describes the model data in a standardized manner (not needed to build the QNN model later on). +4. Location of Artifacts: + * You can find the artifacts in the `${QNN_SDK_ROOT}/examples/Models/InceptionV3/model` directory. + +### Other Model Types: +If you are using another type of model, you can refer to the [Tools page](https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/tools.html) for a table of potential scripts to help convert them into QNN format. These scripts will have a similar qnn-model-type-converter naming convention. + +### Step 3: Model Build on Linux Host + +1. Choose Target Architecture: + * Select the most relevant supported target architecture from the following list: + + 64-bit Linux targets: `x86_64-linux-clang` + + 64-bit Android devices: `aarch64-android` + + Qualcomm’s QNX OS: `aarch64-qnx` (Note: This architecture is not supported by default in the QNN SDK.) + + OpenEmbedded Linux (GCC 11.2): `aarch64-oe-linux-gcc11.2` + + OpenEmbedded Linux (GCC 9.3): `aarch64-oe-linux-gcc9.3` + + OpenEmbedded Linux (GCC 8.2): `aarch64-oe-linux-gcc8.2` + + Ubuntu Linux (GCC 9.4): `aarch64-ubuntu-gcc9.4` + + Ubuntu Linux (GCC 7.5): `aarch64-ubuntu-gcc7.5` +2. Set Target Architecture: + * On your host machine, set the target architecture of your target device by running: + + `export QNN_TARGET_ARCH="your-target-architecture-from-above"` + * For **Android**: + + `export QNN_TARGET_ARCH="aarch64-android"` + * For **Linux Embedded**: + + `export QNN_TARGET_ARCH="aarch64-oe-linux-gcc11.2"` +3. Generate Model Library: + * Run the following command on your host machine: + ``` + python3 "${QNN_SDK_ROOT}/bin/x86_64-linux-clang/qnn-model-lib-generator" \ + -c "${QNN_SDK_ROOT}/examples/Models/InceptionV3/model/Inception_v3.cpp" \ + -b "${QNN_SDK_ROOT}/examples/Models/InceptionV3/model/Inception_v3.bin" \ + -o "${QNN_SDK_ROOT}/examples/Models/InceptionV3/model_libs" \ + -t ${QNN_TARGET_ARCH} + ``` + * `-c`: Path to the `.cpp` QNN model file. + * `-b`: Path to the `.bin` QNN model file (optional but recommended). + * `-o`: Path to the output folder. + * `-t`: Target architecture to build for. +4. Verify Output File: + * Run: + + `ls ${QNN_SDK_ROOT}/examples/Models/InceptionV3/model_libs/${QNN_TARGET_ARCH}` + * Verify that the output file `libInception_v3.so` is inside. This file will be used on the target device to execute inferences. The output `.so` file will be located in the `model_libs` directory, named according to the target architecture. + +### Step 4: Model Build on Linux Host + +Users can set custom options and different performance modes to the HTP Backend through the backend config. Follow these steps to create the necessary JSON files: + +1. Create Backend Config File (`be.json`): + * For **Android** with the following content: + ```json + { + "graphs": [ + { + "graph_names": [ + "Inception_v3" + ], + "vtcm_mb": 8 + } + ], + "devices": [ + { + "htp_arch": "v79" + } + ] + } + ``` + * For **Linux Embedded** with the following content: + ```json + { + "graphs": [ + { + "graph_names": [ + "Inception_v3" + ], + "vtcm_mb": 8 + } + ], + "devices": [ + { + "htp_arch": "v73" + } + ] + } + ``` +2. Create Backend Extensions Config File (`config.json`): + * The config file with minimum parameters specified through JSON is given below: + ```json + { + "backend_extensions": { + "shared_library_path": "./libQnnHtpNetRunExtensions.so", + "config_file_path": "./be.json" + } + } + ``` +3. Place JSON Files in the Model Libraries Path: + * Ensure that the `be.json` and `config.json` files are located in the following path: + + `${QNN_SDK_ROOT}/examples/Models/InceptionV3/model_libs/${QNN_TARGET_ARCH}` + +### Step 5: Prepare the Target Device + +1. Open a Terminal on the Target Device: + * Run the following commands to create a destination folder and set the necessary permissions: + ``` + adb shell "mkdir -p /data/local/tmp" + adb shell "ln -s /etc/ /data/local/tmp" + adb shell "chmod -R 777 /data/local/tmp" + adb shell "mkdir -p /data/local/tmp/inception_v3" + ``` +2. Push Necessary Files: + * Push `libQnnHtp.so` and other necessary executables, input data, and input list from your host machine to `/data/local/tmp/inception_v3` on the target device: + ``` + adb push \path\to\qairt\2.32.0.250228\lib\${QNN_TARGET_ARCH} /data/local/tmp/inception_v3 + adb shell "cd /data/local/tmp/inception_v3; mv ./${QNN_TARGET_ARCH}/* ./; rmdir ${QNN_TARGET_ARCH}" + ``` + * For **Android**: + + `adb push \path\to\qairt\2.32.0.250228\lib\hexagon-v79\unsigned /data/local/tmp/inception_v3` + * For **Linux Embedded**: + + `adb push \path\to\qairt\2.32.0.250228\lib\hexagon-v73\unsigned /data/local/tmp/inception_v3` + + * Continue remaining steps on the target device: + ``` + adb shell "cd /data/local/tmp/inception_v3; mv ./unsigned/* ./; rmdir unsigned" + adb push \path\to\qairt\2.32.0.250228\examples\Models\InceptionV3\model_libs\${QNN_TARGET_ARCH}\ /data/local/tmp/inception_v3 + adb shell "cd /data/local/tmp/inception_v3; mv ./${QNN_TARGET_ARCH}/* ./; rmdir ${QNN_TARGET_ARCH}" + ``` + ``` + adb push \path\to\qairt\2.32.0.250228\examples\Models\InceptionV3\data\cropped /data/local/tmp/inception_v3 + adb push \path\to\qairt\2.32.0.250228\examples\Models\InceptionV3\data\target_raw_list.txt /data/local/tmp/inception_v3 + adb push \path\to\qairt\2.32.0.250228\bin\${QNN_TARGET_ARCH}\qnn-net-run /data/local/tmp/inception_v3 + adb shell "chmod 777 /data/local/tmp/inception_v3/qnn-net-run" + ``` +3. Run the Model & Pull Output Directory to Host Machine: + * On the target device, run the following command to execute the model: + ``` + adb shell "LD_LIBRARY_PATH=/data/local/tmp/inception_v3 DSP_LIBRARY_PATH=/data/local/tmp/inception_v3; cd /data/local/tmp/inception_v3; ./qnn-net-run --model ./libInception_v3.so --input_list ./target_raw_list.txt --backend ./libQnnHtp.so --output_dir ./output --config_file ./config.json" + ``` + * Run the following command to pull the output directory from the target device to your host machine: + + `adb pull /data/local/tmp/inception_v3/output \path\to\qairt\2.32.0.250228\examples\Models\InceptionV3` + +### Step 6: View Classification Result + +1. Navigate to the Example Directory on Host Machine: + * Run: + + `cd ${QNN_SDK_ROOT}/examples/Models/InceptionV3` +2. Run the Script to View Classification Results: + * Execute the following command to view the classification results: + ``` + python3 "./scripts/show_inceptionv3_classifications.py" \ + -i "./data/cropped/raw_list.txt" \ + -o "./output" \ + -l "./data/imagenet_slim_labels.txt" + ``` + * Ensure that the classification results in the output match the following: + ``` + ${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/notice_sign.raw 0.152344 459 brass + ${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/chairs.raw 0.281250 832 studio couch + ${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/trash_bin.raw 0.800781 413 ashcan + ${QNN_SDK_ROOT}/examples/Models/InceptionV3/data/cropped/plastic_cup.raw 0.988281 648 measuring cup + ``` + +### References + +* [Linux Setup](https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/linux_setup.html) +* [CNN to QNN for Linux Host](https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/qnn_tutorial_linux_host.html) * [CNN to QNN for Linux Host on Linux Target](https://docs.qualcomm.com/bundle/publicresource/topics/80-63442-50/qnn_tutorial_linux_host_linux_target.html) \ No newline at end of file From 028d21ccefdc028fd264bde158102fd46359f980 Mon Sep 17 00:00:00 2001 From: Jianping Li Date: Fri, 15 Aug 2025 15:18:03 +0800 Subject: [PATCH 02/40] Clean-up: Remove unused Android-specific DSP HAL code Cleand up unused DSP HAL code that was specific to Android and not used in current implementation. Signed-off-by: Jianping Li --- src/Makefile.am | 8 +++---- src/adsprpcd.c | 17 +------------- src/cdsprpcd.c | 17 +------------- src/dspqueue/.dirstamp | 0 src/fastrpc_apps_user.c | 52 ++--------------------------------------- 5 files changed, 8 insertions(+), 86 deletions(-) create mode 100644 src/dspqueue/.dirstamp diff --git a/src/Makefile.am b/src/Makefile.am index 8f7a243..d108f7f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,6 +1,6 @@ lib_LTLIBRARIES = -LIBDSPRPC_CFLAGS = -fno-short-enums -U_DEBUG -DARM_ARCH_7A -DLE_ENABLE -DNO_HAL -DENABLE_UPSTREAM_DRIVER_INTERFACE -DUSE_SYSLOG -I$(top_srcdir)/inc +LIBDSPRPC_CFLAGS = -fno-short-enums -U_DEBUG -DARM_ARCH_7A -DLE_ENABLE -DENABLE_UPSTREAM_DRIVER_INTERFACE -DUSE_SYSLOG -I$(top_srcdir)/inc LIBDSPRPC_SOURCES = \ fastrpc_apps_user.c \ @@ -102,19 +102,19 @@ bin_PROGRAMS = adsprpcd cdsprpcd sdsprpcd adsprpcddir = $(libdir) adsprpcd_SOURCES = adsprpcd.c adsprpcd_DEPENDENCIES = libadsp_default_listener.la -adsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=0 -DUSE_SYSLOG -DNO_HAL +adsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=0 -DUSE_SYSLOG adsprpcd_LDADD = -ldl $(USE_LOG) cdsprpcddir = $(libdir) cdsprpcd_SOURCES = cdsprpcd.c cdsprpcd_DEPENDENCIES = libcdsp_default_listener.la -cdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=3 -DUSE_SYSLOG -DNO_HAL +cdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=3 -DUSE_SYSLOG cdsprpcd_LDADD = -ldl $(USE_LOG) sdsprpcddir = $(libdir) sdsprpcd_SOURCES = sdsprpcd.c sdsprpcd_DEPENDENCIES = libsdsp_default_listener.la -sdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=2 -DUSE_SYSLOG -DNO_HAL +sdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=2 -DUSE_SYSLOG sdsprpcd_LDADD = -ldl $(USE_LOG) diff --git a/src/adsprpcd.c b/src/adsprpcd.c index 452c645..16ab9df 100644 --- a/src/adsprpcd.c +++ b/src/adsprpcd.c @@ -17,9 +17,6 @@ #ifndef ADSP_DEFAULT_LISTENER_NAME #define ADSP_DEFAULT_LISTENER_NAME "libadsp_default_listener.so" #endif -#ifndef ADSP_LIBHIDL_NAME -#define ADSP_LIBHIDL_NAME "libhidlbase.so" -#endif typedef int (*adsp_default_listener_start_t)(int argc, char *argv[]); @@ -27,16 +24,10 @@ int main(int argc, char *argv[]) { int nErr = 0; void *adsphandler = NULL; -#ifndef NO_HAL - void *libhidlbaseHandler = NULL; -#endif adsp_default_listener_start_t listener_start; VERIFY_EPRINTF("adsp daemon starting"); -#ifndef NO_HAL - if(NULL != (libhidlbaseHandler = dlopen(ADSP_LIBHIDL_NAME, RTLD_NOW))) { -#endif - while (1) { + while (1) { if(NULL != (adsphandler = dlopen(ADSP_DEFAULT_LISTENER_NAME, RTLD_NOW))) { if(NULL != (listener_start = (adsp_default_listener_start_t)dlsym(adsphandler, "adsp_default_listener_start"))) { @@ -54,13 +45,7 @@ int main(int argc, char *argv[]) { } VERIFY_EPRINTF("adsp daemon will restart after 25ms..."); usleep(25000); - } -#ifndef NO_HAL - if(0 != dlclose(libhidlbaseHandler)) { - VERIFY_EPRINTF("libhidlbase dlclose failed"); - } } -#endif VERIFY_EPRINTF("adsp daemon exiting %x", nErr); return nErr; diff --git a/src/cdsprpcd.c b/src/cdsprpcd.c index bb01ad8..2a6e1e6 100644 --- a/src/cdsprpcd.c +++ b/src/cdsprpcd.c @@ -16,9 +16,6 @@ #ifndef CDSP_DEFAULT_LISTENER_NAME #define CDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so" #endif -#ifndef CDSP_LIBHIDL_NAME -#define CDSP_LIBHIDL_NAME "libhidlbase.so" -#endif typedef int (*adsp_default_listener_start_t)(int argc, char *argv[]); @@ -26,16 +23,10 @@ int main(int argc, char *argv[]) { int nErr = 0; void *cdsphandler = NULL; -#ifndef NO_HAL - void *libhidlbaseHandler = NULL; -#endif adsp_default_listener_start_t listener_start; VERIFY_EPRINTF("cdsp daemon starting"); -#ifndef NO_HAL - if (NULL != (libhidlbaseHandler = dlopen(CDSP_LIBHIDL_NAME, RTLD_NOW))) { -#endif - while (1) { + while (1) { if (NULL != (cdsphandler = dlopen(CDSP_DEFAULT_LISTENER_NAME, RTLD_NOW))) { if (NULL != (listener_start = (adsp_default_listener_start_t)dlsym( @@ -55,13 +46,7 @@ int main(int argc, char *argv[]) { } VERIFY_EPRINTF("cdsp daemon will restart after 100ms..."); usleep(100000); - } -#ifndef NO_HAL - if (0 != dlclose(libhidlbaseHandler)) { - VERIFY_EPRINTF("libhidlbase dlclose failed"); - } } -#endif VERIFY_EPRINTF("cdsp daemon exiting %x", nErr); return nErr; diff --git a/src/dspqueue/.dirstamp b/src/dspqueue/.dirstamp new file mode 100644 index 0000000..e69de29 diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index f2f56f7..4a79982 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -73,9 +73,6 @@ #include "shared.h" #include "verify.h" #include "fastrpc_context.h" -#ifndef NO_HAL -#include "DspClient.h" -#endif #include "fastrpc_process_attributes.h" #include "fastrpc_trace.h" @@ -326,11 +323,6 @@ static bool fastrpc_notif_flag = false; static int fastrpc_trace = 0; static uint32_t fastrpc_wake_lock_enable[NUM_DOMAINS_EXTEND] = {0}; -#ifndef NO_HAL -static pthread_mutex_t dsp_client_mut; -static void *dsp_client_instance[NUM_SESSIONS]; -#endif - static int domain_init(int domain, int *dev); static void domain_deinit(int domain); static int close_device_node(int domain_id, int dev); @@ -3321,21 +3313,6 @@ int open_device_node(int domain_id) { "falling back to node %s \n", get_domain_name(domain), domain, strerror(errno), DEFAULT_DEVICE); dev = open(DEFAULT_DEVICE, O_NONBLOCK); -#ifndef NO_HAL - if ((dev < 0) && (errno == EACCES)) { - FARF(ALWAYS, - "%s: no access to default device of domain %d, open thru HAL, " - "(sess_id %d)\n", - __func__, domain, sess_id); - VERIFYC(sess_id < NUM_SESSIONS, AEE_EBADITEM); - pthread_mutex_lock(&dsp_client_mut); - if (!dsp_client_instance[sess_id]) { - dsp_client_instance[sess_id] = create_dsp_client_instance(); - } - pthread_mutex_unlock(&dsp_client_mut); - dev = open_hal_session(dsp_client_instance[sess_id], domain_id); - } -#endif } } break; @@ -3355,23 +3332,8 @@ int open_device_node(int domain_id) { static int close_device_node(int domain_id, int dev) { int nErr = 0; - -#ifndef NO_HAL - int domain = GET_DOMAIN_FROM_EFFEC_DOMAIN_ID(domain_id); - int sess_id = GET_SESSION_ID_FROM_DOMAIN_ID(domain_id); - if ((domain == CDSP_DOMAIN_ID) || - (domain == CDSP1_DOMAIN_ID)) && - dsp_client_instance[sess_id]) { - nErr = close_hal_session(dsp_client_instance[sess_id], domain_id, dev); - FARF(ALWAYS, "%s: close device %d thru HAL on session %d\n", __func__, dev, - sess_id); - } else { -#endif - nErr = close(dev); - FARF(ALWAYS, "%s: closed dev %d on domain %d", __func__, dev, domain_id); -#ifndef NO_HAL - } -#endif + nErr = close(dev); + FARF(ALWAYS, "%s: closed dev %d on domain %d", __func__, dev, domain_id); return nErr; } @@ -4115,13 +4077,6 @@ static void fastrpc_apps_user_deinit(void) { free(hlist); hlist = NULL; } -#ifndef NO_HAL - for (i = 0; i < NUM_SESSIONS; i++) { - destroy_dsp_client_instance(dsp_client_instance[i]); - dsp_client_instance[i] = NULL; - } - pthread_mutex_destroy(&dsp_client_mut); -#endif fastrpc_context_table_deinit(); deinit_process_signals(); fastrpc_notif_deinit(); @@ -4192,9 +4147,6 @@ static int fastrpc_apps_user_init(void) { fastrpc_log_init(); fastrpc_config_init(); pthread_mutex_init(&update_notif_list_mut, 0); -#ifndef NO_HAL - pthread_mutex_init(&dsp_client_mut, 0); -#endif VERIFYC(NULL != (hlist = calloc(NUM_DOMAINS_EXTEND, sizeof(*hlist))), AEE_ENOMEMORY); FOR_EACH_EFFECTIVE_DOMAIN_ID(i) { From 2ce1792430ed062e1b9bde1c9cef8d088ffbab26 Mon Sep 17 00:00:00 2001 From: Abhinav Parihar Date: Thu, 14 Aug 2025 14:53:32 +0530 Subject: [PATCH 03/40] rpcmem: Register rpcmem-allocated buffers with fastrpc memory framework Currently, rpcmem-allocated buffers used in remote calls are not recognized by the RPC kernel driver, resulting in fallback to copy buffers for message passing. This fallback introduces significant performance degradation, which worsens with increasing buffer sizes. This commit ensures that rpcmem-allocated buffers are properly registered with the fastrpc memory framework upon allocation and deregistered upon release. By enabling the kernel driver to identify the associated file descriptors, it avoids the use of copy buffers during remote invocation, thereby improving performance and efficiency. Signed-off-by: Abhinav Parihar --- inc/fastrpc_mem.h | 18 ++++++++++++++++++ src/rpcmem_linux.c | 3 +++ 2 files changed, 21 insertions(+) diff --git a/inc/fastrpc_mem.h b/inc/fastrpc_mem.h index 06a9f98..e4b8667 100644 --- a/inc/fastrpc_mem.h +++ b/inc/fastrpc_mem.h @@ -55,4 +55,22 @@ int remote_mmap64_internal(int fd, uint32_t flags, uint64_t vaddrin, int64_t siz */ int fastrpc_buffer_ref(int domain, int fd, int ref, void **va, size_t *size); + +/* + * Register or deregister a buffer with fastrpc framework, managing its reference count + * and associated metadata. + * + * @param buf Pointer to the buffer to be registered or deregistered + * @param size Size of the buffer in bytes + * @param fd File descriptor associated with the buffer; use -1 to deregister + + * This function internally calls `remote_register_buf_common` with default attributes. + * - If `fd` is not -1, the buffer is registered and its reference count is incremented. + * - If `fd` is -1, the buffer is deregistered and its reference count is decremented. + * If the reference count reaches zero, the buffer is unmapped and its metadata is freed. + * + * @return void + */ +void remote_register_buf(void *buf, int size, int fd); + #endif //FASTRPC_MEM_H diff --git a/src/rpcmem_linux.c b/src/rpcmem_linux.c index a4b5ed0..a0814b9 100644 --- a/src/rpcmem_linux.c +++ b/src/rpcmem_linux.c @@ -50,6 +50,7 @@ #include "fastrpc_ioctl.h" #include "rpcmem.h" #include "verify.h" +#include "fastrpc_mem.h" #define PAGE_SIZE 4096 @@ -202,6 +203,7 @@ void *rpcmem_alloc_internal(int heapid, uint32_t flags, size_t size) { pthread_mutex_unlock(&rpcmt); FARF(RUNTIME_RPC_HIGH, "Allocted memory from DMA heap fd %d ptr %p orig ptr %p\n", rinfo->fd, rinfo->aligned_buf, rinfo->buf); + remote_register_buf(rinfo->buf, rinfo->size, rinfo->fd); return rinfo->aligned_buf; bail: if (nErr) { @@ -230,6 +232,7 @@ void rpcmem_free_internal(void *po) { } pthread_mutex_unlock(&rpcmt); if (rfree) { + remote_register_buf(rfree->buf, rfree->size, -1); munmap(rfree->buf, rfree->size); close(rfree->fd); free(rfree); From d2ec9f5b6ac3472ed0c3056b1af3faf5e57268b7 Mon Sep 17 00:00:00 2001 From: Jianping Li Date: Fri, 8 Aug 2025 11:45:16 +0800 Subject: [PATCH 04/40] Have a single source file for all daemons Refactor DSP RPC daemons: unify adsprpcd, cdsprpcd, and sdsprpcd into a single source file This change consolidates the daemon implementations into dsprpcd.c to reduce code duplication and simplify future maintenance. Also unified the restart time handling across all daemons to ensure consistent behavior. HIDL support is removed, as it was Android-specific and not utilized in the current DSP setup. Signed-off-by: Jianping Li --- src/Makefile.am | 14 ++++----- src/adsprpcd.c | 52 --------------------------------- src/cdsprpcd.c | 53 ---------------------------------- src/dsprpcd.c | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ src/sdsprpcd.c | 50 -------------------------------- 5 files changed, 84 insertions(+), 162 deletions(-) delete mode 100644 src/adsprpcd.c delete mode 100644 src/cdsprpcd.c create mode 100644 src/dsprpcd.c delete mode 100644 src/sdsprpcd.c diff --git a/src/Makefile.am b/src/Makefile.am index d108f7f..c3c042a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -96,25 +96,25 @@ libsdsp_default_listener_la_DEPENDENCIES = libsdsprpc.la libsdsp_default_listener_la_LDFLAGS = libsdsprpc.la -shared -module $(USE_LOG) -version-number @LT_VERSION_NUMBER@ libsdsp_default_listener_la_CFLAGS = $(SDSP_CFLAGS) -DUSE_SYSLOG - bin_PROGRAMS = adsprpcd cdsprpcd sdsprpcd adsprpcddir = $(libdir) -adsprpcd_SOURCES = adsprpcd.c +adsprpcd_SOURCES = dsprpcd.c adsprpcd_DEPENDENCIES = libadsp_default_listener.la -adsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=0 -DUSE_SYSLOG +adsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=0 -DUSE_SYSLOG -DUSE_ADSP adsprpcd_LDADD = -ldl $(USE_LOG) cdsprpcddir = $(libdir) -cdsprpcd_SOURCES = cdsprpcd.c +cdsprpcd_SOURCES = dsprpcd.c cdsprpcd_DEPENDENCIES = libcdsp_default_listener.la -cdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=3 -DUSE_SYSLOG +cdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=3 -DUSE_SYSLOG -DUSE_CDSP cdsprpcd_LDADD = -ldl $(USE_LOG) sdsprpcddir = $(libdir) -sdsprpcd_SOURCES = sdsprpcd.c +sdsprpcd_SOURCES = dsprpcd.c sdsprpcd_DEPENDENCIES = libsdsp_default_listener.la -sdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=2 -DUSE_SYSLOG +sdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=2 -DUSE_SYSLOG -DUSE_SDSP sdsprpcd_LDADD = -ldl $(USE_LOG) + diff --git a/src/adsprpcd.c b/src/adsprpcd.c deleted file mode 100644 index 16ab9df..0000000 --- a/src/adsprpcd.c +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2024, Qualcomm Innovation Center, Inc. All rights reserved. -// SPDX-License-Identifier: BSD-3-Clause - -#ifndef VERIFY_PRINT_ERROR -#define VERIFY_PRINT_ERROR -#endif -#define VERIFY_PRINT_INFO 0 - -#include -#include -#include -#include "verify.h" -#include "HAP_farf.h" -#include "AEEStdErr.h" - - -#ifndef ADSP_DEFAULT_LISTENER_NAME -#define ADSP_DEFAULT_LISTENER_NAME "libadsp_default_listener.so" -#endif - -typedef int (*adsp_default_listener_start_t)(int argc, char *argv[]); - -int main(int argc, char *argv[]) { - - int nErr = 0; - void *adsphandler = NULL; - adsp_default_listener_start_t listener_start; - - VERIFY_EPRINTF("adsp daemon starting"); - while (1) { - if(NULL != (adsphandler = dlopen(ADSP_DEFAULT_LISTENER_NAME, RTLD_NOW))) { - if(NULL != (listener_start = - (adsp_default_listener_start_t)dlsym(adsphandler, "adsp_default_listener_start"))) { - VERIFY_IPRINTF("adsp_default_listener_start called"); - nErr = listener_start(argc, argv); - } - if(0 != dlclose(adsphandler)) { - VERIFY_EPRINTF("dlclose failed"); - } - } else { - VERIFY_EPRINTF("adsp daemon error %s", dlerror()); - } - if (nErr == AEE_ECONNREFUSED) { - VERIFY_EPRINTF("fastRPC device driver is disabled, retrying..."); - } - VERIFY_EPRINTF("adsp daemon will restart after 25ms..."); - usleep(25000); - } - VERIFY_EPRINTF("adsp daemon exiting %x", nErr); - - return nErr; -} diff --git a/src/cdsprpcd.c b/src/cdsprpcd.c deleted file mode 100644 index 2a6e1e6..0000000 --- a/src/cdsprpcd.c +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2024, Qualcomm Innovation Center, Inc. All rights reserved. -// SPDX-License-Identifier: BSD-3-Clause - -#ifndef VERIFY_PRINT_ERROR -#define VERIFY_PRINT_ERROR -#endif -#define VERIFY_PRINT_INFO 0 - -#include "AEEStdErr.h" -#include "HAP_farf.h" -#include "verify.h" -#include -#include -#include - -#ifndef CDSP_DEFAULT_LISTENER_NAME -#define CDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so" -#endif - -typedef int (*adsp_default_listener_start_t)(int argc, char *argv[]); - -int main(int argc, char *argv[]) { - - int nErr = 0; - void *cdsphandler = NULL; - adsp_default_listener_start_t listener_start; - - VERIFY_EPRINTF("cdsp daemon starting"); - while (1) { - if (NULL != - (cdsphandler = dlopen(CDSP_DEFAULT_LISTENER_NAME, RTLD_NOW))) { - if (NULL != (listener_start = (adsp_default_listener_start_t)dlsym( - cdsphandler, "adsp_default_listener_start"))) { - VERIFY_IPRINTF("cdsp_default_listener_start called"); - nErr = listener_start(argc, argv); - } - if (0 != dlclose(cdsphandler)) { - VERIFY_EPRINTF("dlclose failed"); - } - } else { - VERIFY_EPRINTF("cdsp daemon error %s", dlerror()); - } - if (nErr == AEE_ECONNREFUSED) { - VERIFY_EPRINTF("fastRPC device driver is disabled, daemon exiting..."); - break; - } - VERIFY_EPRINTF("cdsp daemon will restart after 100ms..."); - usleep(100000); - } - VERIFY_EPRINTF("cdsp daemon exiting %x", nErr); - - return nErr; -} diff --git a/src/dsprpcd.c b/src/dsprpcd.c new file mode 100644 index 0000000..df86bc1 --- /dev/null +++ b/src/dsprpcd.c @@ -0,0 +1,77 @@ +// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef VERIFY_PRINT_ERROR +#define VERIFY_PRINT_ERROR +#endif +#define VERIFY_PRINT_INFO 0 + +#include "AEEStdErr.h" +#include "HAP_farf.h" +#include "verify.h" +#include "fastrpc_common.h" +#include +#include +#include +#include + +#ifndef ADSP_DEFAULT_LISTENER_NAME +#define ADSP_DEFAULT_LISTENER_NAME "libadsp_default_listener.so" +#endif +#ifndef CDSP_DEFAULT_LISTENER_NAME +#define CDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so" +#endif +#ifndef SDSP_DEFAULT_LISTENER_NAME +#define SDSP_DEFAULT_LISTENER_NAME "libsdsp_default_listener.so" +#endif + +typedef int (*dsp_default_listener_start_t)(int argc, char *argv[]); + +int main(int argc, char *argv[]) { + int nErr = 0; + void *dsphandler = NULL; + const char* lib_name; + const char* dsp_name; + dsp_default_listener_start_t listener_start; + + #ifdef USE_ADSP + lib_name = ADSP_DEFAULT_LISTENER_NAME; + dsp_name = "ADSP"; + #elif defined(USE_SDSP) + lib_name = SDSP_DEFAULT_LISTENER_NAME; + dsp_name = "SDSP"; + #elif defined(USE_CDSP) + lib_name = CDSP_DEFAULT_LISTENER_NAME; + dsp_name = "CDSP"; + #else + goto bail; + #endif + VERIFY_EPRINTF("%s daemon starting", dsp_name); + + while (1) { + if (NULL != (dsphandler = dlopen(lib_name,RTLD_NOW))) { + if (NULL != (listener_start = (dsp_default_listener_start_t)dlsym( + dsphandler, "adsp_default_listener_start"))) { + VERIFY_IPRINTF("adsp_default_listener_start called"); + nErr = listener_start(argc, argv); + } + if (0 != dlclose(dsphandler)) { + VERIFY_EPRINTF("dlclose failed for %s", lib_name); + } + } else { + VERIFY_EPRINTF("%s daemon error %s", dsp_name, dlerror()); + } + + if (nErr == AEE_ECONNREFUSED) { + VERIFY_EPRINTF("fastRPC device is not accessible, daemon exiting..."); + break; + } + + VERIFY_EPRINTF("%s daemon will restart after 100ms...", dsp_name); + usleep(100000); + } + + bail: + VERIFY_EPRINTF("daemon exiting %x", nErr); + return nErr; +} diff --git a/src/sdsprpcd.c b/src/sdsprpcd.c deleted file mode 100644 index 1e8a109..0000000 --- a/src/sdsprpcd.c +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved. -// SPDX-License-Identifier: BSD-3-Clause - -#ifndef VERIFY_PRINT_ERROR -#define VERIFY_PRINT_ERROR -#endif - -#include -#include -#include -#include "verify.h" -#include "AEEStdErr.h" - - -#ifndef SDSP_DEFAULT_LISTENER_NAME -#define SDSP_DEFAULT_LISTENER_NAME "libsdsp_default_listener.so" -#endif - -typedef int (*adsp_default_listener_start_t)(int argc, char *argv[]); - -int main(int argc, char *argv[]) { - - int nErr = 0; - void *sdsphandler = NULL; - adsp_default_listener_start_t listener_start; - - VERIFY_EPRINTF("sdsp daemon starting"); - while (1) { - if(NULL != (sdsphandler = dlopen(SDSP_DEFAULT_LISTENER_NAME, RTLD_NOW))) { - if(NULL != (listener_start = - (adsp_default_listener_start_t)dlsym(sdsphandler, "adsp_default_listener_start"))) { - VERIFY_IPRINTF("sdsp_default_listener_start called"); - listener_start(argc, argv); - } - if(0 != dlclose(sdsphandler)) { - VERIFY_EPRINTF("dlclose failed"); - } - } else { - VERIFY_EPRINTF("sdsp daemon error %s", dlerror()); - } - if (nErr == AEE_ECONNREFUSED) { - VERIFY_EPRINTF("fastRPC device driver is disabled, retrying..."); - } - VERIFY_EPRINTF("sdsp daemon will restart after 100ms..."); - usleep(100000); - } - VERIFY_EPRINTF("sdsp daemon exiting %x", nErr); -bail: - return nErr; -} From 333bd54da98c61487c5f8192b819fe2324f7e646 Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Thu, 28 Aug 2025 12:59:53 +0530 Subject: [PATCH 05/40] CI: Docker-based pre-merge pipeline for FastRPC This commit introduces a foundational CI pipeline for FastRPC, enabling automated sync, build, and test processes using Docker and AWS infrastructure. Key changes: - Added `aws_s3_helper` composite action to support single/multi-upload and download modes, generate presigned URLs, and manage artifact uploads to S3 - Implemented `build_docker_image` composite action to clone and build the FastRPC Docker image - Created `build` workflow using AWS runner to compile kernel artifacts, package outputs (Image, ramdisk, dtb, modules), and upload to S3 - Developed `build_workspace` composite action to prepare DLKM packaging and integrate with systemd-boot - Added `test_action` composite action to parse artifact URLs, update metadata, inject links into cloudData, and generate LAVA job definitions - Established `test` workflow to trigger LAVA jobs and collect results using the generated job definition This pipeline ensures consistent pre-merge validation and artifact handling for FastRPC development. Signed-off-by: Tharun Kumar Merugu --- .github/actions/aws_s3_helper/action.yml | 99 ++++++++++ .github/actions/build/action.yml | 123 ++++++++++++ .github/actions/build_docker_image/action.yml | 71 +++++++ .github/actions/lava_job_render/action.yml | 183 ++++++++++++++++++ .github/actions/loading/action.yml | 54 ++++++ .github/actions/sync/action.yml | 128 ++++++++++++ .github/workflows/loading.yml | 28 +++ .github/workflows/pre_merge.yml | 45 +++++ .github/workflows/sync_build.yml | 107 ++++++++++ .github/workflows/test.yml | 142 ++++++++++++++ ci/MACHINES.json | 4 + 11 files changed, 984 insertions(+) create mode 100644 .github/actions/aws_s3_helper/action.yml create mode 100644 .github/actions/build/action.yml create mode 100644 .github/actions/build_docker_image/action.yml create mode 100644 .github/actions/lava_job_render/action.yml create mode 100644 .github/actions/loading/action.yml create mode 100644 .github/actions/sync/action.yml create mode 100644 .github/workflows/loading.yml create mode 100644 .github/workflows/pre_merge.yml create mode 100644 .github/workflows/sync_build.yml create mode 100644 .github/workflows/test.yml create mode 100644 ci/MACHINES.json diff --git a/.github/actions/aws_s3_helper/action.yml b/.github/actions/aws_s3_helper/action.yml new file mode 100644 index 0000000..ad3bdda --- /dev/null +++ b/.github/actions/aws_s3_helper/action.yml @@ -0,0 +1,99 @@ +name: AWS S3 Helper +description: Upload and download files from AWS S3 + +inputs: + s3_bucket: + description: S3 Bucket Name + required: true + local_file: + description: Local file paths. For 'multi-upload', this should be a path to a file containing a list of local file paths (one per line). For 'single-upload', it's a single local file path. + required: false + default: ../artifacts/file_list.txt + download_file: + description: S3 path of the file to download (e.g., path/to/your/file.txt). Used only in 'download' mode. + required: false + default: '' + download_location: + description: Local directory where the downloaded file should be placed. Used only in 'download' mode. + required: false + default: . + mode: + description: Mode of operation (single-upload, multi-upload, download) + required: true + default: single-upload + machine: + description: Target machine name or identifier, used to organize uploads in S3. + type: string + required: true + +outputs: + presigned_url: + description: Pre-signed URL for the single uploaded file (only available in 'single-upload' mode). + value: ${{ steps.sync-data.outputs.presigned_url }} + +runs: + using: "composite" + steps: + - name: Sync Data + id: sync-data + shell: bash + env: + UPLOAD_LOCATION: ${{ github.repository_owner }}/${{ github.event.repository.name }}/${{ github.workflow }}/${{ github.head_ref != '' && github.head_ref || github.run_id }}/${{ inputs.machine }}/ + run: | + echo "::group::$(printf '__________ %-100s' 'Process' | tr ' ' _)" + case "${{ inputs.mode }}" in + multi-upload) + echo "Uploading files to S3 bucket..." + first_line=true + # Start the JSON object + mkdir -p "${{ github.workspace }}/${{ inputs.machine }}" + echo "{" > ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + while IFS= read -r file; do + if [ -f "$file" ]; then + echo "Uploading $file..." + aws s3 cp "$file" s3://${{ inputs.s3_bucket }}/${{ env.UPLOAD_LOCATION }} + echo "Uploaded $file to s3://${{ inputs.s3_bucket }}/${{ env.UPLOAD_LOCATION }}" + echo "Creating Pre-signed URL for $file..." + filename=$(basename "$file") + presigned_url=$(aws s3 presign s3://${{ inputs.s3_bucket }}/${{ env.UPLOAD_LOCATION }}$filename --expires-in 259200) + if [ "$first_line" = true ]; then + first_line=false + else + echo "," >> ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + fi + # Append the pre-signed URL to the file + echo " \"${file}\": \"${presigned_url}\"" >> ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + echo "Pre-signed URL for $file: $presigned_url" + else + echo "Warning: $file does not exist or is not a regular file." + fi + done < "${{ inputs.local_file }}" + # Close the JSON object + echo "}" >> ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + ;; + single-upload) + echo "Uploading single file to S3 bucket..." + aws s3 cp "${{ inputs.local_file }}" s3://${{ inputs.s3_bucket }}/${{ env.UPLOAD_LOCATION }} + echo "Uploaded ${{ inputs.local_file }} to s3://${{ inputs.s3_bucket }}/${{ env.UPLOAD_LOCATION }}" + echo "Creating Pre-signed URL for ${{ inputs.local_file }}..." + presigned_url=$(aws s3 presign s3://${{ inputs.s3_bucket }}/${{ env.UPLOAD_LOCATION }}${{ inputs.local_file }} --expires-in 259200) + echo "presigned_url=${presigned_url}" >> "$GITHUB_OUTPUT" + ;; + download) + #Download The required file from s3 + echo "Downloading files from S3 bucket..." + aws s3 cp s3://${{ inputs.s3_bucket }}/${{ inputs.download_file }} ${{ inputs.download_location }} + ;; + *) + echo "Invalid mode. Use 'upload' or 'download'." + exit 1 + ;; + esac + + - name: Upload artifacts + if: ${{ inputs.mode == 'multi-upload' }} + uses: actions/upload-artifact@v4 + with: + name: presigned_urls_${{ inputs.machine }}.json + path: ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + retention-days: 3 diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 0000000..6b90942 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,123 @@ +name: Build workspace +description: | + Builds and packages the FastRPC workspace. + This composite action compiles the FastRPC source code and kernel modules + using a specified Docker image, verifies the compilation artifacts, and + then packages the resulting binaries, libraries, and kernel modules + into a gzipped CPIO ramdisk. It also includes steps to copy + firmware files and test binaries into the ramdisk. + Finally, it unpacks the generated ramdisk for inspection and verification. + +inputs: + docker_image: + description: Docker image + required: true + default: fastrpc-image:latest + workspace_path: + description: Workspace path + required: true + +runs: + using: "composite" + steps: + - name: Compile fastrpc code using Docker Image + shell: bash + run: | + cd ${{ inputs.workspace_path }} + echo "::group::$(printf '__________ %-100s' 'Compile fastrpc' | tr ' ' _)" + docker run -i --rm \ + --user $(id -u):$(id -g) \ + --workdir="$PWD" \ + -v "$(dirname $PWD)":"$(dirname $PWD)" \ + ${{ inputs.docker_image }} bash -c " + ./gitcompile --host=aarch64-linux-gnu + " + echo "::endgroup::" + + echo "Verify the compiled fastrpc files" + Files=( + src/.libs/libadsp_default_listener.so + src/.libs/libadsprpc.so + src/.libs/libcdsp_default_listener.so + src/.libs/libcdsprpc.so + src/.libs/libsdsp_default_listener.so + src/.libs/libsdsprpc.so + src/adsprpcd + src/cdsprpcd + src/sdsprpcd + ) + for File in "${Files[@]}" + do + if [ -f "$File" ] ; then echo "$File - Exists" ; else echo "$File - Not Exists" && exit 1 ; fi + done + + - name: Build kernel using Docker Image + shell: bash + run: | + cd ${{ inputs.workspace_path }}/kernel + + echo "::group::$(printf '__________ %-100s' 'Compile kernel' | tr ' ' _)" + docker run -i --rm \ + --user $(id -u):$(id -g) \ + --workdir="$PWD" \ + -v "$(dirname $PWD)":"$(dirname $PWD)" \ + ${{ inputs.docker_image }} bash -c " + make O=../kobj defconfig + make O=../kobj -j$(nproc) + make O=../kobj -j$(nproc) dir-pkg INSTALL_MOD_STRIP=1 + " + echo "::endgroup::" + + - name: Package Files into ramdisk + shell: bash + run: | + # Create directories for pushing fastrpc binaries + mkdir -p ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib + mkdir -p ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin + + cd ${{ inputs.workspace_path }} + + echo "Copy fastrpc files" + cp -rf src/.libs/libadsp_default_listener.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ + cp -rf src/.libs/libadsprpc.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ + cp -rf src/.libs/libcdsp_default_listener.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ + cp -rf src/.libs/libcdsprpc.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ + cp -rf src/.libs/libsdsp_default_listener.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ + cp -rf src/.libs/libsdsprpc.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ + cp -rf src/adsprpcd src/cdsprpcd src/sdsprpcd ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin/ + + echo "Copy fastrpc test files" + cp -rf test/fastrpc_test ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin + cp -rf test/linux/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin + cp -rf test/v75/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin + + echo "Package FastRPC firmware files" + cp -r ${{ inputs.workspace_path }}/firmware_dir/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/ + + echo "Package DLKM into ramdisk" + cd ${{ inputs.workspace_path }}/kobj/tar-install + cp -r lib/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ + ls -la ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ + + cd ${{ inputs.workspace_path }}/artifacts/ + find ramdisk_fastrpc | cpio -o -H newc > ramdisk_fastrpc.cpio + gzip -9 ramdisk_fastrpc.cpio + mv ramdisk_fastrpc.cpio.gz ramdisk_fastrpc.gz + + - name: Unpack and inspect ramdisk + shell: bash + run: | + echo "Unpack and mount ramdisk" + cd ${{ inputs.workspace_path }}/artifacts + + # Decompress the ramdisk.gz safely + gunzip -c ramdisk_fastrpc.gz > ramdisk_test/ramdisk + + echo "Unpacking ramdisk" + cd ramdisk_test + # Use safer syntax to avoid broken pipe + cpio -idmv < ramdisk || echo "cpio unpack failed" + + # Inspect contents + ls -ltr usr/lib | grep rpc || true + ls -ltr usr/bin | grep rpc || true diff --git a/.github/actions/build_docker_image/action.yml b/.github/actions/build_docker_image/action.yml new file mode 100644 index 0000000..7a1787e --- /dev/null +++ b/.github/actions/build_docker_image/action.yml @@ -0,0 +1,71 @@ +name: Build fastrpc docker image +description: | + Build fastrpc docker image + This GitHub Action builds the fastrpc Docker image using a multi-step process: + Checks out the fastrpc-image repository to obtain the Dockerfile. + Sets up Docker Buildx for advanced build capabilities. + Captures the current user's UID, GID, and username for use as Docker build arguments. + Builds the Docker image with caching enabled, tagging it as 'fastrpc-image:latest'. + Outputs a success message and the image name after the build. + Re-checks out the main repository for any subsequent workflow steps. + +inputs: + image: + description: The docker image to Build + required: true + default: fastrpc-image:latest + +runs: + using: "composite" + steps: + # Checkout fastrpc-image repository to get the Dockerfile + - name: Checkout fastrpc-image repo for Dockerfile + uses: actions/checkout@v4 + with: + fetch-depth: 0 + repository: qualcomm/fastrpc-image + ref: main + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Pre-Build Docker Image + id: pre-build + shell: bash + run: | + uid=$(id -u) + gid=$(id -g) + user=$(whoami) + echo "uid=$uid" >> "$GITHUB_OUTPUT" + echo "gid=$gid" >> "$GITHUB_OUTPUT" + echo "user=$user" >> "$GITHUB_OUTPUT" + echo "Running as user: $user (UID: $uid, GID: $gid)" + + - name: Build fastrpc Docker Image + id: build + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: true + tags: fastrpc-image:latest + build-args: | + "USER=${{ steps.pre-build.outputs.user }}" + "UID=${{ steps.pre-build.outputs.uid }}" + "GID=${{ steps.pre-build.outputs.gid }}" + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Post-Build Docker Image + id: post-build + shell: bash + run: | + echo "Docker image fastrpc-image:latest built successfully." + echo "image_name=fastrpc-image:latest" >> $GITHUB_OUTPUT + + # Checkout the main repo again for later steps + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref }} \ No newline at end of file diff --git a/.github/actions/lava_job_render/action.yml b/.github/actions/lava_job_render/action.yml new file mode 100644 index 0000000..eb1b287 --- /dev/null +++ b/.github/actions/lava_job_render/action.yml @@ -0,0 +1,183 @@ +name: Test Action +description: | + This GitHub Action composite action prepares data for LAVA job definitions. + It reads presigned URLs from a JSON file, creates a metadata.json file, + uploads it to S3, and then populates a cloudData.json file with various + artifact URLs (image, vmlinux, firmware, modules, dtb, metadata, ramdisk). + Finally, it generates a LAVA job definition using these populated data files. + +inputs: + docker_image: + description: Docker image + required: true + default: fastrpc-image:latest + +runs: + using: "composite" + steps: + - name: Process presigned_urls.json + id: process_urls + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const p = require('path'); + // Helper function to find URL by filename + function findUrlByFilename(filename) { + for (const [path, url] of Object.entries(data)) { + if (path.endsWith(filename)) { + return url; + } + } + return null; + } + const filePath = p.join( + process.env.GITHUB_WORKSPACE, + `presigned_urls_${process.env.MACHINE}.json` + ); + + if (fs.existsSync(filePath)) { + console.log("File exists"); + } else { + console.log("File does not exist"); + core.setFailed(`File not found: ${filePath}`); + } + // Read the JSON file + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + // Extract URLs into variables + const modulesTarUrl = findUrlByFilename('modules.tar.xz'); + const imageUrl = findUrlByFilename('Image'); + const vmlinuxUrl = findUrlByFilename('vmlinux'); + const firmwareUrl = findUrlByFilename('ramdisk_fastrpc.gz'); + const dtbFilename = `${process.env.MACHINE}.dtb`; + const dtbUrl = findUrlByFilename(dtbFilename); + // Set outputs + core.setOutput('modules_url', modulesTarUrl); + core.setOutput('image_url', imageUrl); + core.setOutput('vmlinux_url', vmlinuxUrl); + core.setOutput('firmware_url', firmwareUrl); + core.setOutput('dtb_url', dtbUrl); + console.log(`Modules URL: ${modulesTarUrl}`); + console.log(`Image URL: ${imageUrl}`); + console.log(`Vmlinux URL: ${vmlinuxUrl}`); + console.log(`Ramdisk FastRPC URL: ${firmwareUrl}`); + console.log(`Dtb URL: ${dtbUrl}`); + + - name: Create metadata.json + id: create_metadata + shell: bash + run: | + echo "Creating job dtb definition" + # Create the job definition using the processed URLs + cd ../job_render + docker run -i --rm \ + --user "$(id -u):$(id -g)" \ + --workdir="$PWD" \ + -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ + -e dtb_url="${{ steps.process_urls.outputs.dtb_url }}" \ + ${{ inputs.docker_image }} \ + jq '.artifacts["dtbs/qcom/${{ env.MACHINE }}.dtb"] = env.dtb_url' data/metadata.json > temp.json && mv temp.json data/metadata.json + + - name: Upload metadata.json + id: upload_metadata + uses: qualcomm/fastrpc/.github/actions/aws_s3_helper@main + with: + local_file: ../job_render/data/metadata.json + s3_bucket: qli-prd-fastrpc-gh-artifacts + mode: single-upload + + - name: Create cloudData json + shell: bash + run: | + echo "Creating job metadata definition" + metadata_url="${{ steps.upload_metadata.outputs.presigned_url }}" + ramdisk_url="${{ steps.process_urls.outputs.ramdisk_url }}" + firmware_url="${{ steps.process_urls.outputs.firmware_url }}" + vmlinux_url="${{ steps.process_urls.outputs.vmlinux_url }}" + image_url="${{ steps.process_urls.outputs.image_url }}" + modules_url="${{ steps.process_urls.outputs.modules_url }}" + + # Create the job definition using the processed URLs + cd ../job_render + echo "Creating metadata_url ${metadata_url}" + # using metadata_url + docker run -i --rm \ + --user "$(id -u):$(id -g)" \ + --workdir="$PWD" \ + -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ + -e metadata_url="$metadata_url" \ + ${{ inputs.docker_image }} \ + jq '.artifacts.metadata = env.metadata_url' data/cloudData.json > temp.json && mv temp.json data/cloudData.json + + echo "Creating image_url ${image_url}" + # using image_url + docker run -i --rm \ + --user "$(id -u):$(id -g)" \ + --workdir="$PWD" \ + -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ + -e image_url="$image_url" \ + ${{ inputs.docker_image }} \ + jq '.artifacts.kernel = env.image_url' data/cloudData.json > temp.json && mv temp.json data/cloudData.json + + echo "Creating vmlinux_url ${vmlinux_url}" + # using vmlinux_url + docker run -i --rm \ + --user "$(id -u):$(id -g)" \ + --workdir="$PWD" \ + -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ + -e vmlinux_url="$vmlinux_url" \ + ${{ inputs.docker_image }} \ + jq '.artifacts.vmlinux = env.vmlinux_url' data/cloudData.json > temp.json && mv temp.json data/cloudData.json + + echo "Creating FastRPC firmware_url ${firmware_url}" + # using firmware_url - ramdisk_fastrpc + docker run -i --rm \ + --user "$(id -u):$(id -g)" \ + --workdir="$PWD" \ + -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ + -e firmware_url="$firmware_url" \ + ${{ inputs.docker_image }} \ + jq '.artifacts.firmware = env.firmware_url' data/cloudData.json > temp.json && mv temp.json data/cloudData.json + + echo "Creating modules_url ${modules_url}" + # using modules_url + docker run -i --rm \ + --user "$(id -u):$(id -g)" \ + --workdir="$PWD" \ + -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ + -e modules_url="$modules_url" \ + ${{ inputs.docker_image }} \ + jq '.artifacts.modules = env.modules_url' data/cloudData.json > temp.json && mv temp.json data/cloudData.json + + - name: Update firmware to cloudData + shell: bash + run: | + cd ../job_render + ramdisk_url="$(aws s3 presign s3://qli-prd-fastrpc-gh-artifacts/meta-qcom/initramfs-kerneltest-full-image-qcom-armv8a.cpio.gz --expires 7600)" + echo "Updating ramdisk_url ${ramdisk_url}" + # using ramdisk_url + docker run -i --rm \ + --user "$(id -u):$(id -g)" \ + --workdir="$PWD" \ + -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ + -e ramdisk_url="$ramdisk_url" \ + ${{ inputs.docker_image }} \ + jq '.artifacts.ramdisk = env.ramdisk_url' data/cloudData.json > temp.json && mv temp.json data/cloudData.json + + - name: Create lava_job_definition + shell: bash + run: | + cd ../job_render + mkdir -p renders + + docker run -i --rm \ + --user "$(id -u):$(id -g)" \ + --workdir="$PWD" \ + -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ + -e TARGET="${{ env.LAVA_NAME }}" \ + -e TARGET_DTB="${{ env.MACHINE }}" \ + ${{ inputs.docker_image }} \ + sh -c 'export BOOT_METHOD=fastboot && \ + export TARGET=${TARGET} && \ + export TARGET_DTB=${TARGET_DTB} && \ + python3 lava_Job_definition_generator.py --localjson ./data/cloudData.json --fastrpc-premerge' diff --git a/.github/actions/loading/action.yml b/.github/actions/loading/action.yml new file mode 100644 index 0000000..c97910f --- /dev/null +++ b/.github/actions/loading/action.yml @@ -0,0 +1,54 @@ +name: Load Parameters +description: | + This composite action loads build parameters from a `MACHINES.json` file. + It parses the JSON file to create two matrices: + - `build_matrix`: A slimmed-down matrix containing only 'machine' and 'firmware' for better visibility in job names. + - `full_matrix`: A complete matrix including 'machine', 'firmware', and 'lavaname' for passing all necessary details to test jobs. + It expects the `MACHINES.json` file to be located at `ci/MACHINES.json` relative to the workspace root. + Each entry in `MACHINES.json` should be a key-value pair where the key is the machine name + and the value is an array `[firmware, lavaname]`. + +outputs: + build_matrix: + description: Build matrix + value: ${{ steps.set-matrix.outputs.build_matrix }} + + full_matrix: + description: full matrix containing lava devails + value: ${{ steps.set-matrix.outputs.full_matrix }} + +runs: + using: "composite" + steps: + - name: Set Build Matrix + id: set-matrix + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const filePath = path.join(process.env.GITHUB_WORKSPACE, 'ci', 'MACHINES.json'); + let file; + try { + if (!fs.existsSync(filePath)) { + core.setFailed(`MACHINES.json not found at ${filePath}`); + return; + } + file = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } catch (err) { + core.setFailed(`Failed to load or parse MACHINES.json: ${err.message}`); + return; + } + # Slim matrix for better job visibility + const slim_matrix = Object.entries(file).map(([machine, [firmware]]) => ({ machine, firmware })); + core.setOutput('build_matrix', JSON.stringify(slim_matrix)); + console.log(slim_matrix); + + # Full matrix to pass to test jobs + const complete_matrix = Object.entries(file).map(([machine, [firmware, lavaname]]) => ({ + machine, + firmware, + lavaname + })); + core.setOutput('full_matrix', JSON.stringify(complete_matrix)); + console.log(complete_matrix); diff --git a/.github/actions/sync/action.yml b/.github/actions/sync/action.yml new file mode 100644 index 0000000..391dfc1 --- /dev/null +++ b/.github/actions/sync/action.yml @@ -0,0 +1,128 @@ +name: Sync workspace +description: | + This composite action synchronizes a workspace by cleaning it up, + checking out necessary code repositories, and preparing a firmware + directory with required binaries. It clones and copies FastRPC hexagon + DSP binaries and Linux firmware (specifically QCOM firmware) + based on the provided `firmware` input. It also clones the + `qualcomm-linux/kernel` repository. + The action sets an output `workspace_path` to the GitHub workspace + directory. + +inputs: + machine: + description: Target machine name + type: string + required: true + firmware: + description: Firmware identifier + type: string + required: true + +outputs: + workspace_path: + description: Sync workspace path + value: ${{ steps.set-workspace.outputs.workspace }} + +runs: + using: "composite" + steps: + - name: Clean workspace + shell: bash + run: | + echo "Cleaning up workspace..." + rm -rf ${{ github.workspace }}/* + echo "Workspace cleaned successfully!" + + - name: Checkout fastrpc code + uses: actions/checkout@v4 + + - name: Clone FastRPC hexagon DSP binaries repositories and copy Firmware + shell: bash + run: | + cd ${{ github.workspace }} + mkdir -p firmware_dir/usr/lib/dsp/ + + echo "Cloning FastRPC hexagon DSP binaries..." + git clone https://github.com/linux-msm/hexagon-dsp-binaries.git + cd hexagon-dsp-binaries + git fetch --all --tags --prune + + TARGET="${{ inputs.firmware }}" + DSP_REF="" + CONFIG="config.txt" + + echo "Searching FastRPC hexagon DSP binaries for target: $TARGET" + if [ ! -f "$CONFIG" ]; then + echo "Error: $CONFIG file not found in hexagon-dsp-binaries directory." + exit 1 + fi + while read -r _ subdir dsp version; do + echo "DSP binaries for $dsp in $subdir - $version" + if [[ "${subdir,,}" == */"${TARGET,,}" ]]; then + echo "Found matching DSP binaries for target '$TARGET': $subdir - $version" + echo "Copying DSP binaries for $dsp in $subdir/$version" + mkdir -p "../firmware_dir/usr/lib/dsp/${dsp}" + cp -rf "$subdir/$version/"* "../firmware_dir/usr/lib/dsp/${dsp}/" + echo "Copied DSP binaries for $dsp in ../firmware_dir/usr/lib/dsp/${dsp}" + DSP_REF="$version" + echo "${dsp}:${subdir}////${version}" + fi + done < <(grep "^Install:" "$CONFIG") + echo "DSP binaries copied successfully!" + + - name: Clone Firmware binaries repositories and copy Firmware + shell: bash + run: | + cd ${{ github.workspace }} + git clone https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git + TARGET_RAW="${{ inputs.firmware }}" + # Remove "-ride" suffix if present + TARGET="${TARGET_RAW%-ride}" + + # Construct the source path for qcom firmware in the cloned linux-firmware repo + # It's usually under /qcom/ + LINUX_FIRMWARE_QCOM_PATH="linux-firmware/qcom/${TARGET}" + + # Check if the target qcom firmware directory exists in the cloned repo + if [ ! -d "$LINUX_FIRMWARE_QCOM_PATH" ]; then + echo "ERROR: Directory '$LINUX_FIRMWARE_QCOM_PATH' not found in linux-firmware repo." + exit -1 + else + # Create the destination directory in firmware_dir + mkdir -p "${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${TARGET}" + + # Copy the contents of the qcom target directory + # Use trailing slashes to copy contents, not the directory itself + cp -rf "${LINUX_FIRMWARE_QCOM_PATH}/"* "${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${TARGET}/" + echo "Copied QCOM firmware from '${LINUX_FIRMWARE_QCOM_PATH}' to '${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${TARGET}/' successfully!" + fi + + - name: List all subdirectories and files in firmware_dir + shell: bash + run: | + cd ${{ github.workspace }} + echo "Listing contents of firmware_dir recursively..." + find firmware_dir -type d -print + find firmware_dir -type f -print + + - name: Configure git + shell: bash + run: | + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + + - name: Clone kernel repositories + shell: bash + run: | + cd ${{ github.workspace }} + git clone https://github.com/qualcomm-linux/kernel.git + cd kernel + git fetch origin + git checkout qcom-next + + - name: Set workspace path + id: set-workspace + shell: bash + run: | + echo "workspace=${{ github.workspace }}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/loading.yml b/.github/workflows/loading.yml new file mode 100644 index 0000000..7e5d665 --- /dev/null +++ b/.github/workflows/loading.yml @@ -0,0 +1,28 @@ +--- +name: _loading +description: Load required parameters for the subsequent jobs + +on: + workflow_call: + outputs: + build_matrix: + description: Build matrix + value: ${{ jobs.loading.outputs.build_matrix }} + + full_matrix: + description: Full Matrix containing lava description + value: ${{ jobs.loading.outputs.full_matrix }} + +jobs: + loading: + runs-on: ubuntu-latest + outputs: + build_matrix: ${{ steps.loading.outputs.build_matrix }} + full_matrix: ${{ steps.loading.outputs.full_matrix }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Load Parameters + id: loading + uses: qualcomm/fastrpc/.github/actions/loading@main diff --git a/.github/workflows/pre_merge.yml b/.github/workflows/pre_merge.yml new file mode 100644 index 0000000..4285987 --- /dev/null +++ b/.github/workflows/pre_merge.yml @@ -0,0 +1,45 @@ +name: pre_merge +on: + push: + branches: + - 'main' + - 'development' + pull_request: + types: [opened, synchronize, reopened] + branches: + - 'main' + - 'development' + workflow_dispatch: + +jobs: + + # The loading job will load the build matrix from MACHINES.json + loading: + uses: qualcomm/fastrpc/.github/workflows/loading.yml@main + secrets: inherit + + # Each job in build and test will run for every {machine, firmware} pair + # The build job will run on the machines specified in the build_matrix output + # from the loading job. It will use the build.yml workflow to build the code. + build: + needs: loading + strategy: + matrix: + include: ${{ fromJSON(needs.loading.outputs.build_matrix) }} + uses: qualcomm/fastrpc/.github/workflows/sync_build.yml@main + secrets: inherit + with: + docker_image: fastrpc-image:latest + machine: ${{ matrix.machine }} + firmware: ${{ matrix.firmware }} + + # The test job will run on the machines specified in the full_matrix output + # from the loading job. It will use the test.yml workflow to run tests. + test: + needs: [loading, build] + uses: qualcomm/fastrpc/.github/workflows/test.yml@main + secrets: inherit + with: + docker_image: fastrpc-image:latest + build_matrix: ${{ needs.loading.outputs.build_matrix }} + full_matrix: ${{ needs.loading.outputs.full_matrix }} diff --git a/.github/workflows/sync_build.yml b/.github/workflows/sync_build.yml new file mode 100644 index 0000000..2e9352f --- /dev/null +++ b/.github/workflows/sync_build.yml @@ -0,0 +1,107 @@ +name: Sync and build +description: sync and builds fastrpc code for a specified machine using a Docker image + +on: + workflow_call: + inputs: + docker_image: + description: Docker image + type: string + required: true + machine: + description: Target machine name + type: string + required: true + firmware: + description: Firmware identifier + type: string + required: true + +jobs: + build: + runs-on: + group: GHA-fastrpc-Prd-SelfHosted-RG + labels: [ self-hosted, fastrpc-prd-u2404-x64-large-od-ephem ] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # fetch-depth: 1 is generally sufficient for builds and is faster than 0 (full history). + # Change to 0 if your build process strictly requires the full Git history. + fetch-depth: 1 + + - name: Build fastrpc docker image + uses: qualcomm/fastrpc/.github/actions/build_docker_image@main + with: + image: ${{ inputs.docker_image }} + + - name: Sync codebase + id: sync + uses: qualcomm/fastrpc/.github/actions/sync@main + with: + machine: ${{ inputs.machine }} + firmware: ${{ inputs.firmware }} + + - name: Build workspace + id: build_workspace + uses: qualcomm/fastrpc/.github/actions/build@main + with: + docker_image: ${{ inputs.docker_image }} + workspace_path: ${{ steps.sync.outputs.workspace_path }} + + - name: Create file list for artifacts upload + run: | + workspace=${{ steps.sync.outputs.workspace_path }} + + # Create the tarball in the root of the GitHub workspace + cd "$workspace/kobj/tar-install" + tar -cJf "${{ github.workspace }}/modules.tar.xz" lib/modules/ + + # Populate the file_list.txt with paths to artifacts + cat < "$workspace/artifacts/file_list.txt" + ${{ github.workspace }}/modules.tar.xz + $workspace/kobj/arch/arm64/boot/Image + $workspace/kobj/vmlinux + $workspace/artifacts/ramdisk_fastrpc.gz + $workspace/kobj/arch/arm64/boot/dts/qcom/${{ inputs.machine }}.dtb + EOF + + echo "Generated artifact list:" + cat "$workspace/artifacts/file_list.txt" + + - name: Upload artifacts + uses: qualcomm/fastrpc/.github/actions/aws_s3_helper@main + with: + s3_bucket: qli-prd-fastrpc-gh-artifacts + # local_file points to the list of files to upload + local_file: ${{ steps.sync.outputs.workspace_path }}/artifacts/file_list.txt + mode: multi-upload + machine: ${{ inputs.machine }} + + - name: Clean up + # This step is crucial for self-hosted runners to manage disk space. + run: | + rm -rf "${{ steps.sync.outputs.workspace_path }}/artifacts" || true + rm -rf "${{ steps.sync.outputs.workspace_path }}/fastrpc_libs" || true + rm -rf "${{ steps.sync.outputs.workspace_path }}/gcc-linaro-7.5.0-2019.12-i686_aarch64-linux-gnu" || true + rm -rf "${{ steps.sync.outputs.workspace_path }}/kobj" || true + rm -f "${{ github.workspace }}/modules.tar.xz" || true # Clean up the tarball from the root workspace + + - name: Update summary + # This step always runs to provide feedback on the build status. + if: success() || failure() + shell: bash + run: | + if [ "${{ steps.build_workspace.outcome }}" == 'success' ]; then + echo "Build was successful" + summary=":heavy_check_mark: Build Success" + else + echo "Build failed" + summary=":x: Build Failed" + fi + SUMMARY=' +
Build Summary + '${summary}' +
+ ' + echo -e "$SUMMARY" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0a9b8f0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,142 @@ +name: test + +description: | + Run tests on LAVA for a specified machine using a Docker image. + This reusable GitHub Actions workflow executes FastRPC tests on LAVA infrastructure. + It leverages a Docker image for test execution and supports multi-target builds via a matrix strategy. + The workflow automates job creation, submission, and result validation on LAVA, + providing a streamlined testing process. + Designed to be triggered by other workflows, it offers a summary of test outcomes + with direct links to LAVA job results, enhancing traceability and debugging. + +on: + workflow_call: + inputs: + docker_image: + description: Docker image + type: string + required: true + default: fastrpc-image:latest + + build_matrix: + description: Build matrix for multi target builds + type: string + required: true + + full_matrix: + description: Full matrix containing lava description + type: string + required: true + +jobs: + test: + runs-on: + group: GHA-fastrpc-Prd-SelfHosted-RG + labels: [ self-hosted, fastrpc-prd-u2404-x64-large-od-ephem ] + strategy: + fail-fast: false + matrix: + build_matrix: ${{ fromJson(inputs.build_matrix) }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + + - name: Build fastrpc docker image + uses: qualcomm/fastrpc/.github/actions/build_docker_image@main + with: + image: ${{ inputs.docker_image }} + + - name: Download URLs list + uses: actions/download-artifact@v4 + with: + name: presigned_urls_${{ matrix.build_matrix.machine }}.json + merge-multiple: true + path: ${{ github.workspace }} + + - name: Clone lava job render scripts + run: cd .. && git clone https://github.com/qualcomm-linux/job_render + + - name: Extract the LAVA machine name + id: get_lavaname + uses: actions/github-script@v7 + with: + script: | + const fullMatrix = JSON.parse(`${{ inputs.full_matrix }}`); + const currentMachine = `${{ matrix.build_matrix.machine }}`; + + const entry = fullMatrix.find(item => item.machine === currentMachine); + if (!entry) { + core.setFailed(`No entry found in full matrix for machine: ${currentMachine}`); + return; + } + + const lavaname = entry.lavaname; + console.log(`Lavaname for ${currentMachine} is ${lavaname}`); + core.setOutput("LAVANAME", lavaname); + + - name: Create fastrpc - lava job definition + uses: qualcomm/fastrpc/.github/actions/lava_job_render@main + id: create_job_definition + with: + docker_image: ${{ inputs.docker_image }} + env: + FIRMWARE: ${{ matrix.build_matrix.firmware }} + MACHINE: ${{ matrix.build_matrix.machine }} + LAVA_NAME: ${{ steps.get_lavaname.outputs.LAVANAME }} + + - name: Submit lava job + id: submit_job + run: | + cd ../job_render + job_id=$(docker run -i --rm --workdir="$PWD" -v "$(dirname $PWD)":"$(dirname $PWD)" ${{ inputs.docker_image }} sh -c "lavacli identities add --token ${{ secrets.LAVA_OSS_TOKEN }} --uri https://lava-oss.qualcomm.com/RPC2 --username ${{ secrets.LAVA_OSS_USER }} production && lavacli -i production jobs submit ./renders/lava_job_definition.yaml") + job_url="https://lava-oss.qualcomm.com/scheduler/job/$job_id" + echo "job_id=$job_id" >> $GITHUB_OUTPUT + echo "job_url=$job_url" >> $GITHUB_OUTPUT + echo "Lava Job: $job_url" + echo "JOB_ID=$job_id" >> $GITHUB_ENV + + - name: Check lava job results + id: check_job + run: | + STATE="" + while [ "$STATE" != "Finished" ]; do + state=$(docker run -i --rm --workdir="$PWD" -v "$(dirname $PWD)":"$(dirname $PWD)" ${{ inputs.docker_image }} sh -c "lavacli identities add --token ${{ secrets.LAVA_OSS_TOKEN }} --uri https://lava-oss.qualcomm.com/RPC2 --username ${{ secrets.LAVA_OSS_USER }} production && lavacli -i production jobs show $JOB_ID" | grep state) + STATE=$(echo "$state" | cut -d':' -f2 | sed 's/^ *//;s/ *$//') + echo "Current status: $STATE" + sleep 30 + done + health=$(docker run -i --rm --workdir="$PWD" -v "$(dirname $PWD)":"$(dirname $PWD)" ${{ inputs.docker_image }} sh -c "lavacli identities add --token ${{ secrets.LAVA_OSS_TOKEN }} --uri https://lava-oss.qualcomm.com/RPC2 --username ${{ secrets.LAVA_OSS_USER }} production && lavacli -i production jobs show $JOB_ID" | grep Health) + HEALTH=$(echo "$health" | cut -d':' -f2 | sed 's/^ *//;s/ *$//') + if [[ "$HEALTH" == "Complete" ]]; then + echo "Lava job passed." + summary=":heavy_check_mark: Lava job passed." + echo "summary=$summary" >> $GITHUB_OUTPUT + exit 0 + else + echo "Lava job failed." + summary=":x: Lava job failed." + echo "summary=$summary" >> $GITHUB_OUTPUT + exit 1 + fi + + - name: Update summary + if: success() || failure() + shell: bash + run: | + if [ "${{ steps.create_job_definition.conclusion }}" == 'failure' ]; then + status=":x: Test job failed" + else + status="${{ steps.check_job.outputs.summary }}" + job_url="${{ steps.submit_job.outputs.job_url }}" + job_id="${{ steps.submit_job.outputs.job_id }}" + fi + SUMMARY=' +
'${status}' +
+ JOB ID: '${job_id}' +
+ ' + echo -e "$SUMMARY" >> $GITHUB_STEP_SUMMARY diff --git a/ci/MACHINES.json b/ci/MACHINES.json new file mode 100644 index 0000000..7ef482c --- /dev/null +++ b/ci/MACHINES.json @@ -0,0 +1,4 @@ +{ + "qcs9100-ride-r3": ["sa8775p-ride", "qcs9100-ride"], + "qcs8300-ride": ["qcs8300-ride", "qcs8300-ride"] +} From 1ee463dca9750911700a2b9b616a603bd6b1d08b Mon Sep 17 00:00:00 2001 From: Ling Xu Date: Wed, 3 Sep 2025 08:24:42 +0530 Subject: [PATCH 06/40] Add changes to support GPDSPs offloading FastRPC library supports 5 domains. There are some products where new domains, GPDSP0 and GPDSP1, are supported. Add changes to support GPDSP domains. Signed-off-by: Ling Xu --- inc/fastrpc_ioctl.h | 4 ++++ inc/remote.h | 16 +++++++++----- src/Makefile.am | 19 +++++++++++++++- src/adsp_default_listener.c | 20 ++++++++++++++++- src/dsprpcd.c | 6 ++++++ src/fastrpc_apps_user.c | 43 ++++++++++++++++++++++++++++++++----- src/fastrpc_cap.c | 6 +++--- src/fastrpc_ioctl.c | 6 ++++++ 8 files changed, 105 insertions(+), 15 deletions(-) diff --git a/inc/fastrpc_ioctl.h b/inc/fastrpc_ioctl.h index a15b320..779d034 100644 --- a/inc/fastrpc_ioctl.h +++ b/inc/fastrpc_ioctl.h @@ -31,11 +31,15 @@ #define MDSPRPC_DEVICE "/dev/fastrpc-mdsp" #define CDSPRPC_DEVICE "/dev/fastrpc-cdsp" #define CDSP1RPC_DEVICE "/dev/fastrpc-cdsp1" +#define GDSP0RPC_DEVICE "/dev/fastrpc-gdsp0" +#define GDSP1RPC_DEVICE "/dev/fastrpc-gdsp1" #define ADSPRPC_SECURE_DEVICE "/dev/fastrpc-adsp-secure" #define SDSPRPC_SECURE_DEVICE "/dev/fastrpc-sdsp-secure" #define MDSPRPC_SECURE_DEVICE "/dev/fastrpc-mdsp-secure" #define CDSPRPC_SECURE_DEVICE "/dev/fastrpc-cdsp-secure" #define CDSP1RPC_SECURE_DEVICE "/dev/fastrpc-cdsp1-secure" +#define GDSP0RPC_SECURE_DEVICE "/dev/fastrpc-gdsp0-secure" +#define GDSP1RPC_SECURE_DEVICE "/dev/fastrpc-gdsp1-secure" #define FASTRPC_ATTR_NOVA (256) diff --git a/inc/remote.h b/inc/remote.h index 69448f4..1fa3baa 100644 --- a/inc/remote.h +++ b/inc/remote.h @@ -122,6 +122,8 @@ extern "C" { #define SDSP_DOMAIN_ID 2 #define CDSP_DOMAIN_ID 3 #define CDSP1_DOMAIN_ID 4 +#define GDSP0_DOMAIN_ID 5 +#define GDSP1_DOMAIN_ID 6 /** Supported Domain Names */ #define ADSP_DOMAIN_NAME "adsp" @@ -129,6 +131,8 @@ extern "C" { #define SDSP_DOMAIN_NAME "sdsp" #define CDSP_DOMAIN_NAME "cdsp" #define CDSP1_DOMAIN_NAME "cdsp1" +#define GDSP0_DOMAIN_NAME "gdsp0" +#define GDSP1_DOMAIN_NAME "gdsp1" /** Defines to prepare URI for multi-domain calls */ #define ADSP_DOMAIN "&_dom=adsp" @@ -136,6 +140,8 @@ extern "C" { #define SDSP_DOMAIN "&_dom=sdsp" #define CDSP_DOMAIN "&_dom=cdsp" #define CDSP1_DOMAIN "&_dom=cdsp1" +#define GDSP0_DOMAIN "&_dom=gdsp0" +#define GDSP1_DOMAIN "&_dom=gdsp1" /** Internal transport prefix */ #define ITRANSPORT_PREFIX "'\":;./\\" @@ -280,7 +286,7 @@ struct remote_rpc_control_latency { * @brief Argument to query DSP capability with request ID DSPRPC_GET_DSP_INFO */ typedef struct remote_dsp_capability { - uint32_t domain; /** @param[in]: DSP domain ADSP_DOMAIN_ID, SDSP_DOMAIN_ID, or CDSP_DOMAIN_ID */ + uint32_t domain; /** @param[in]: DSP domain ADSP_DOMAIN_ID, SDSP_DOMAIN_ID, CDSP_DOMAIN_ID, or GDSP_DOMAIN_ID */ uint32_t attribute_ID; /** @param[in]: One of the DSP/kernel attributes from enum remote_dsp_attributes */ uint32_t capability; /** @param[out]: Result of the DSP/kernel capability query based on attribute_ID */ }fastrpc_capability; @@ -493,7 +499,7 @@ typedef int (*fastrpc_notif_fn_t)(void *context, int domain, int session, remote **/ typedef struct remote_rpc_notif_register { void *context; /** @param[in]: Context of the client */ - int domain; /** @param[in]: DSP domain ADSP_DOMAIN_ID, SDSP_DOMAIN_ID, or CDSP_DOMAIN_ID */ + int domain; /** @param[in]: DSP domain ADSP_DOMAIN_ID, SDSP_DOMAIN_ID, CDSP_DOMAIN_ID, or GDSP_DOMAIN_ID */ fastrpc_notif_fn_t notifier_fn; /** @param[in]: Notification function pointer */ } remote_rpc_notif_register_t; @@ -1080,7 +1086,7 @@ __QAIC_REMOTE_EXPORT __QAIC_RETURN int __QAIC_REMOTE(remote_munmap)(__QAIC_IN ui * * @param domain [in] DSP domain ID to map memory to. Use -1 for default domain based on * linked library (lib(a/m/s/c)dsprpc.so). - * Valid domains: ADSP_DOMAIN_ID, MDSP_DOMAIN_ID, SDSP_DOMAIN_ID, CDSP_DOMAIN_ID + * Valid domains: ADSP_DOMAIN_ID, MDSP_DOMAIN_ID, SDSP_DOMAIN_ID, CDSP_DOMAIN_ID, GDSP_DOMAIN_ID * @param fd [in] File descriptor of DMA memory to map * @param flags [in] Mapping flags from enum remote_mem_map_flags * @param virtAddr [in] Virtual address of buffer on CPU side @@ -1167,7 +1173,7 @@ __QAIC_REMOTE_EXPORT __QAIC_RETURN int __QAIC_REMOTE(remote_munmap64)(__QAIC_IN * and CACHE WRITEBACK configuration. Driver will clean cache when buffer is passed in a FastRPC call. * * @param domain [in] DSP domain ID of a fastrpc session. Use -1 for default domain based on linked library. - * Valid domains are ADSP_DOMAIN_ID, MDSP_DOMAIN_ID, SDSP_DOMAIN_ID, or CDSP_DOMAIN_ID. + * Valid domains are ADSP_DOMAIN_ID, MDSP_DOMAIN_ID, SDSP_DOMAIN_ID, CDSP_DOMAIN_ID, or GDSP_DOMAIN_ID. * @param fd [in] DMA memory file descriptor obtained from dma_alloc_fd() or similar DMA allocation APIs. * @param addr [in] Virtual address of the buffer on CPU side. Must be the same address returned by mmap() * when mapping the DMA fd. @@ -1199,7 +1205,7 @@ __QAIC_REMOTE_EXPORT __QAIC_RETURN int __QAIC_REMOTE(fastrpc_mmap)(__QAIC_IN int * The mapping must be removed before closing the DMA file descriptor. * * @param domain [in] DSP domain ID of a fastrpc session. Use -1 for default domain based on linked library. - * Valid domains are ADSP_DOMAIN_ID, MDSP_DOMAIN_ID, SDSP_DOMAIN_ID, or CDSP_DOMAIN_ID. + * Valid domains are ADSP_DOMAIN_ID, MDSP_DOMAIN_ID, SDSP_DOMAIN_ID, CDSP_DOMAIN_ID, or GDSP_DOMAIN_ID. * @param fd [in] DMA memory file descriptor that was used to create the mapping. * @param addr [in] Virtual address of the buffer on CPU side. Must match the address used in fastrpc_mmap(). * @param length [in] Size of buffer in bytes to unmap. Must match the length used in fastrpc_mmap(). diff --git a/src/Makefile.am b/src/Makefile.am index c3c042a..7c28c49 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -96,7 +96,18 @@ libsdsp_default_listener_la_DEPENDENCIES = libsdsprpc.la libsdsp_default_listener_la_LDFLAGS = libsdsprpc.la -shared -module $(USE_LOG) -version-number @LT_VERSION_NUMBER@ libsdsp_default_listener_la_CFLAGS = $(SDSP_CFLAGS) -DUSE_SYSLOG -bin_PROGRAMS = adsprpcd cdsprpcd sdsprpcd +GDSP_CFLAGS = $(LIBDSPRPC_CFLAGS) -DDEFAULT_DOMAIN_ID=5 + +libgdsprpc_la_SOURCES = $(LIBDSPRPC_SOURCES) +libgdsprpc_la_LDFLAGS = -ldl -lm $(USE_LOG) -version-number @LT_VERSION_NUMBER@ +libgdsprpc_la_CFLAGS = $(GDSP_CFLAGS) + +libgdsp_default_listener_la_SOURCES = $(LIBDEFAULT_LISTENER_SOURCES) +libgdsp_default_listener_la_DEPENDENCIES = libcdsprpc.la +libgdsp_default_listener_la_LDFLAGS = libcdsprpc.la -shared -module $(USE_LOG) -version-number @LT_VERSION_NUMBER@ +libgdsp_default_listener_la_CFLAGS = $(GDSP_CFLAGS) -DUSE_SYSLOG + +bin_PROGRAMS = adsprpcd cdsprpcd sdsprpcd gdsprpcd adsprpcddir = $(libdir) adsprpcd_SOURCES = dsprpcd.c @@ -118,3 +129,9 @@ sdsprpcd_DEPENDENCIES = libsdsp_default_listener.la sdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=2 -DUSE_SYSLOG -DUSE_SDSP sdsprpcd_LDADD = -ldl $(USE_LOG) + +gdsprpcddir = $(libdir) +gdsprpcd_SOURCES = dsprpcd.c +gdsprpcd_DEPENDENCIES = libcdsp_default_listener.la +gdsprpcd_CFLAGS = -I$(top_srcdir)/inc -DDEFAULT_DOMAIN_ID=5 -DUSE_SYSLOG -DUSE_GDSP +gdsprpcd_LDADD = -ldl $(USE_LOG) \ No newline at end of file diff --git a/src/adsp_default_listener.c b/src/adsp_default_listener.c index e74e135..e37648a 100644 --- a/src/adsp_default_listener.c +++ b/src/adsp_default_listener.c @@ -38,18 +38,24 @@ #define MDSP_SECURE_DEVICE_NAME "fastrpc-mdsp-secure" #define CDSP_SECURE_DEVICE_NAME "fastrpc-cdsp-secure" #define CDSP1_SECURE_DEVICE_NAME "fastrpc-cdsp1-secure" +#define GDSP0_SECURE_DEVICE_NAME "fastrpc-gdsp0-secure" +#define GDSP1_SECURE_DEVICE_NAME "fastrpc-gdsp1-secure" #define ADSP_DEVICE_NAME "fastrpc-adsp" #define SDSP_DEVICE_NAME "fastrpc-sdsp" #define MDSP_DEVICE_NAME "fastrpc-mdsp" #define CDSP_DEVICE_NAME "fastrpc-cdsp" #define CDSP1_DEVICE_NAME "fastrpc-cdsp1" +#define GDSP0_DEVICE_NAME "fastrpc-gdsp0" +#define GDSP1_DEVICE_NAME "fastrpc-gdsp1" // Array of supported domain names and its corresponding ID's. static domain_t supported_domains[] = {{ADSP_DOMAIN_ID, ADSP_DOMAIN}, {MDSP_DOMAIN_ID, MDSP_DOMAIN}, {SDSP_DOMAIN_ID, SDSP_DOMAIN}, {CDSP_DOMAIN_ID, CDSP_DOMAIN}, - {CDSP1_DOMAIN_ID, CDSP1_DOMAIN}}; + {CDSP1_DOMAIN_ID, CDSP1_DOMAIN}, + {GDSP0_DOMAIN_ID, GDSP0_DOMAIN}, + {GDSP1_DOMAIN_ID, GDSP1_DOMAIN}}; // Get domain name for the domain id. static domain_t *get_domain_uri(int domain_id) { @@ -84,6 +90,12 @@ static const char *get_secure_device_name(int domain_id) { case CDSP1_DOMAIN_ID: name = CDSP1_SECURE_DEVICE_NAME; break; + case GDSP0_DOMAIN_ID: + name = GDSP0_SECURE_DEVICE_NAME; + break; + case GDSP1_DOMAIN_ID: + name = GDSP1_SECURE_DEVICE_NAME; + break; default: name = DEFAULT_DEVICE; break; @@ -112,6 +124,12 @@ static const char *get_default_device_name(int domain_id) { case CDSP1_DOMAIN_ID: name = CDSP1_DEVICE_NAME; break; + case GDSP0_DOMAIN_ID: + name = GDSP0_DEVICE_NAME; + break; + case GDSP1_DOMAIN_ID: + name = GDSP1_DEVICE_NAME; + break; default: name = DEFAULT_DEVICE; break; diff --git a/src/dsprpcd.c b/src/dsprpcd.c index df86bc1..b3d01f8 100644 --- a/src/dsprpcd.c +++ b/src/dsprpcd.c @@ -24,6 +24,9 @@ #ifndef SDSP_DEFAULT_LISTENER_NAME #define SDSP_DEFAULT_LISTENER_NAME "libsdsp_default_listener.so" #endif +#ifndef GDSP_DEFAULT_LISTENER_NAME +#define GDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so.1" +#endif typedef int (*dsp_default_listener_start_t)(int argc, char *argv[]); @@ -43,6 +46,9 @@ int main(int argc, char *argv[]) { #elif defined(USE_CDSP) lib_name = CDSP_DEFAULT_LISTENER_NAME; dsp_name = "CDSP"; + #elif defined(USE_GDSP) + lib_name = GDSP_DEFAULT_LISTENER_NAME; + dsp_name = "GDSP"; #else goto bail; #endif diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index 4a79982..b08596b 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -131,7 +131,7 @@ static void check_multilib_util(void); /* Array to store fastrpc library names. */ static const char *fastrpc_library[NUM_DOMAINS] = { - "libadsprpc.so", "libmdsprpc.so", "libsdsprpc.so", "libcdsprpc.so", "libcdsprpc.so"}; + "libadsprpc.so", "libmdsprpc.so", "libsdsprpc.so", "libcdsprpc.so", "libcdsprpc.so", "libcdsprpc.so", "libcdsprpc.so"}; /* Array to store env variable names. */ static char *fastrpc_dsp_lib_refcnt[NUM_DOMAINS]; @@ -259,7 +259,7 @@ const char *ANDROID_DEBUG_VAR_NAME[] = {"fastrpc.process.attrs", "persist.fastrpc.process.attrs", "ro.build.type"}; -const char *SUBSYSTEM_NAME[] = {"adsp", "mdsp", "sdsp", "cdsp", "cdsp1", "reserved", "reserved", "reserved"}; +const char *SUBSYSTEM_NAME[] = {"adsp", "mdsp", "sdsp", "cdsp", "cdsp1", "gdsp0", "gdsp1", "reserved"}; /* Strings for trace event logging */ #define INVOKE_BEGIN_TRACE_STR "fastrpc_msg: userspace_call: begin" @@ -767,6 +767,12 @@ static int get_domain_from_domain_name(const char *domain_name, } else if (!strncmp(domain_name, SUBSYSTEM_NAME[CDSP_DOMAIN_ID], strlen(SUBSYSTEM_NAME[CDSP_DOMAIN_ID]))) { domain = CDSP_DOMAIN_ID; + } else if (!strncmp(domain_name, SUBSYSTEM_NAME[GDSP0_DOMAIN_ID], + strlen(SUBSYSTEM_NAME[GDSP0_DOMAIN_ID]))) { + domain = GDSP0_DOMAIN_ID; + } else if (!strncmp(domain_name, SUBSYSTEM_NAME[GDSP1_DOMAIN_ID], + strlen(SUBSYSTEM_NAME[GDSP1_DOMAIN_ID]))) { + domain = GDSP1_DOMAIN_ID; } else { FARF(ERROR, "ERROR: %s Invalid domain name: %s\n", __func__, domain_name); } @@ -794,6 +800,12 @@ static const char *get_domain_from_id(int domain_id) { case SDSP_DOMAIN_ID: uri_domain_suffix = SDSP_DOMAIN; break; + case GDSP0_DOMAIN_ID: + uri_domain_suffix = GDSP0_DOMAIN; + break; + case GDSP1_DOMAIN_ID: + uri_domain_suffix = GDSP1_DOMAIN; + break; default: uri_domain_suffix = "invalid domain"; break; @@ -3042,6 +3054,12 @@ int get_domain_from_name(const char *uri, uint32_t type) { } else if (!strncmp(uri, CDSP_DOMAIN_NAME, strlen(CDSP_DOMAIN_NAME))) { domain = CDSP_DOMAIN_ID; + } else if (!strncmp(uri, GDSP0_DOMAIN_NAME, + strlen(GDSP0_DOMAIN_NAME))) { + domain = GDSP0_DOMAIN_ID; + } else if (!strncmp(uri, GDSP1_DOMAIN_NAME, + strlen(GDSP1_DOMAIN_NAME))) { + domain = GDSP1_DOMAIN_ID; } else { domain = INVALID_DOMAIN_ID; FARF(ERROR, "Invalid domain name: %s\n", uri); @@ -3059,6 +3077,10 @@ int get_domain_from_name(const char *uri, uint32_t type) { domain = CDSP1_DOMAIN_ID; } else if (strstr(uri, CDSP_DOMAIN)) { domain = CDSP_DOMAIN_ID; + } else if (strstr(uri, GDSP0_DOMAIN)) { + domain = GDSP0_DOMAIN_ID; + } else if (strstr(uri, GDSP1_DOMAIN)) { + domain = GDSP1_DOMAIN_ID; } else { domain = INVALID_DOMAIN_ID; FARF(ERROR, "Invalid domain name: %s\n", uri); @@ -3148,6 +3170,8 @@ static int attach_guestos(int domain) { case CDSP_DOMAIN_ID: case SDSP_DOMAIN_ID: case CDSP1_DOMAIN_ID: + case GDSP0_DOMAIN_ID: + case GDSP1_DOMAIN_ID: attach = USERPD; break; default: @@ -3253,6 +3277,12 @@ static const char *get_domain_name(int domain_id) { case CDSP1_DOMAIN_ID: name = CDSP1RPC_DEVICE; break; + case GDSP0_DOMAIN_ID: + name = GDSP0RPC_DEVICE; + break; + case GDSP1_DOMAIN_ID: + name = GDSP1RPC_DEVICE; + break; default: name = DEFAULT_DEVICE; break; @@ -3297,6 +3327,8 @@ int open_device_node(int domain_id) { break; case CDSP_DOMAIN_ID: case CDSP1_DOMAIN_ID: + case GDSP0_DOMAIN_ID: + case GDSP1_DOMAIN_ID: dev = open(get_secure_domain_name(domain), O_NONBLOCK); if ((dev < 0) && ((errno == ENOENT) || (errno == EACCES))) { FARF(RUNTIME_RPC_HIGH, @@ -3519,7 +3551,8 @@ static int fastrpc_enable_kernel_optimizations(int domain) { dom = GET_DOMAIN_FROM_EFFEC_DOMAIN_ID(domain); const uint32_t max_concurrency = 25; - if (((dom != CDSP_DOMAIN_ID) && (dom != CDSP1_DOMAIN_ID)) || (hlist[domain].dsppd != USERPD)) + if (((dom != CDSP_DOMAIN_ID) && (dom != CDSP1_DOMAIN_ID) && + (dom != GDSP0_DOMAIN_ID) && (dom != GDSP1_DOMAIN_ID)) || (hlist[domain].dsppd != USERPD)) goto bail; errno = 0; @@ -3972,7 +4005,7 @@ static int domain_init(int domain, int *dev) { VERIFY(AEE_SUCCESS == (nErr = fastrpc_mem_open(domain))); VERIFY(AEE_SUCCESS == (nErr = apps_mem_init(domain))); - if (dom == CDSP_DOMAIN_ID || dom == CDSP1_DOMAIN_ID) { + if (dom == CDSP_DOMAIN_ID || dom == CDSP1_DOMAIN_ID || dom == GDSP0_DOMAIN_ID || dom == GDSP1_DOMAIN_ID) { panic_handle = get_adsp_current_process1_handle(domain); if (panic_handle != INVALID_HANDLE) { int ret = -1; @@ -4282,7 +4315,7 @@ __CONSTRUCTOR_ATTRIBUTE__ static void multidsplib_env_init(void) { const char *local_fastrpc_lib_refcnt[NUM_DOMAINS] = { "FASTRPC_ADSP_REFCNT", "FASTRPC_MDSP_REFCNT", "FASTRPC_SDSP_REFCNT", - "FASTRPC_CDSP_REFCNT", "FASTRPC_CDSP1_REFCNT"}; + "FASTRPC_CDSP_REFCNT", "FASTRPC_CDSP1_REFCNT", "FASTRPC_GDSP0_REFCNT", "FASTRPC_GDSP1_REFCNT"}; char buf[64] = {0}; size_t env_name_len = 0; char *env_name = NULL; diff --git a/src/fastrpc_cap.c b/src/fastrpc_cap.c index 447d44b..0ad169e 100644 --- a/src/fastrpc_cap.c +++ b/src/fastrpc_cap.c @@ -21,13 +21,13 @@ #define BUF_SIZE 50 -const char * RPROC_SUBSYSTEM_NAME[] = {"adsp", "mss", "spss", "cdsp", "cdsp1", "reserved", "reserved", "reserved"}; +const char * RPROC_SUBSYSTEM_NAME[] = {"adsp", "mss", "spss", "cdsp", "cdsp1", "gdsp0", "gdsp1", "reserved"}; static inline uint32_t fastrpc_check_if_dsp_present_pil(uint32_t domain) { uint32_t domain_supported = 0; struct stat sb; // mark rest of the list as reserved to avoid out of bound access - const char *SUBSYSTEM_DEV_NAME[] = {"/dev/subsys_adsp", "", "/dev/subsys_slpi", "/dev/subsys_cdsp", "/dev/subsys_cdsp1", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved"}; + const char *SUBSYSTEM_DEV_NAME[] = {"/dev/subsys_adsp", "", "/dev/subsys_slpi", "/dev/subsys_cdsp", "/dev/subsys_cdsp1", "/dev/subsys_gdsp0", "/dev/subsys_gdsp1", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved", "reserved"}; // If device file is present, then target supports that DSP if (!stat(SUBSYSTEM_DEV_NAME[domain], &sb)) { @@ -47,7 +47,7 @@ static inline uint32_t fastrpc_check_if_dsp_present_rproc(uint32_t domain) { int dir_index = 0, nErr = AEE_SUCCESS; char *buffer = NULL; - if (domain < ADSP_DOMAIN_ID || domain > CDSP1_DOMAIN_ID) { + if (domain < ADSP_DOMAIN_ID || domain > GDSP1_DOMAIN_ID) { FARF(ERROR, "%s Invalid domain 0x%x ", __func__, domain); return 0; } diff --git a/src/fastrpc_ioctl.c b/src/fastrpc_ioctl.c index ba6a29e..e0329e1 100644 --- a/src/fastrpc_ioctl.c +++ b/src/fastrpc_ioctl.c @@ -40,6 +40,12 @@ const char *get_secure_domain_name(int domain_id) { case CDSP1_DOMAIN_ID: name = CDSP1RPC_SECURE_DEVICE; break; + case GDSP0_DOMAIN_ID: + name = GDSP0RPC_SECURE_DEVICE; + break; + case GDSP1_DOMAIN_ID: + name = GDSP1RPC_SECURE_DEVICE; + break; default: name = DEFAULT_DEVICE; break; From 83cfc8ad57edcbeb2979ba50a150957947413686 Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Thu, 4 Sep 2025 20:57:39 +0530 Subject: [PATCH 07/40] CI: Switch all reusable workflow references to 'development' branch (#233) - Ensures CI uses latest logic from development branch Signed-off-by: Tharun Kumar Merugu --- .github/actions/lava_job_render/action.yml | 2 +- .github/workflows/loading.yml | 5 ++--- .github/workflows/pre_merge.yml | 16 +++++++++++++--- .github/workflows/sync_build.yml | 17 +++++++++++------ .github/workflows/test.yml | 5 ++--- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/.github/actions/lava_job_render/action.yml b/.github/actions/lava_job_render/action.yml index eb1b287..5e51cc5 100644 --- a/.github/actions/lava_job_render/action.yml +++ b/.github/actions/lava_job_render/action.yml @@ -80,7 +80,7 @@ runs: - name: Upload metadata.json id: upload_metadata - uses: qualcomm/fastrpc/.github/actions/aws_s3_helper@main + uses: qualcomm/fastrpc/.github/actions/aws_s3_helper@development with: local_file: ../job_render/data/metadata.json s3_bucket: qli-prd-fastrpc-gh-artifacts diff --git a/.github/workflows/loading.yml b/.github/workflows/loading.yml index 7e5d665..952c9ac 100644 --- a/.github/workflows/loading.yml +++ b/.github/workflows/loading.yml @@ -1,5 +1,4 @@ ---- -name: _loading +name: loading description: Load required parameters for the subsequent jobs on: @@ -25,4 +24,4 @@ jobs: - name: Load Parameters id: loading - uses: qualcomm/fastrpc/.github/actions/loading@main + uses: qualcomm/fastrpc/.github/actions/loading@development diff --git a/.github/workflows/pre_merge.yml b/.github/workflows/pre_merge.yml index 4285987..6ba6471 100644 --- a/.github/workflows/pre_merge.yml +++ b/.github/workflows/pre_merge.yml @@ -1,4 +1,14 @@ name: pre_merge +description: | + This workflow defines the pre_merge CI process for the fastrpc repository. + It is triggered on pushes to 'main' or 'development' branches, + on pull request events (opened, synchronize, reopened) targeting 'main' or 'development', + or manually via workflow_dispatch. + Loading: Loads the build matrix from MACHINES.json. + Build: Syncs and Builds code for each machine/firmware pair using sync_build.yml. + Test: Runs LAVA tests using built artifacts via test.yml. + All jobs use reusable workflows and inherit secrets from the caller. + on: push: branches: @@ -15,7 +25,7 @@ jobs: # The loading job will load the build matrix from MACHINES.json loading: - uses: qualcomm/fastrpc/.github/workflows/loading.yml@main + uses: qualcomm/fastrpc/.github/workflows/loading.yml@development secrets: inherit # Each job in build and test will run for every {machine, firmware} pair @@ -26,7 +36,7 @@ jobs: strategy: matrix: include: ${{ fromJSON(needs.loading.outputs.build_matrix) }} - uses: qualcomm/fastrpc/.github/workflows/sync_build.yml@main + uses: qualcomm/fastrpc/.github/workflows/sync_build.yml@development secrets: inherit with: docker_image: fastrpc-image:latest @@ -37,7 +47,7 @@ jobs: # from the loading job. It will use the test.yml workflow to run tests. test: needs: [loading, build] - uses: qualcomm/fastrpc/.github/workflows/test.yml@main + uses: qualcomm/fastrpc/.github/workflows/test.yml@development secrets: inherit with: docker_image: fastrpc-image:latest diff --git a/.github/workflows/sync_build.yml b/.github/workflows/sync_build.yml index 2e9352f..58ea48e 100644 --- a/.github/workflows/sync_build.yml +++ b/.github/workflows/sync_build.yml @@ -1,5 +1,10 @@ -name: Sync and build -description: sync and builds fastrpc code for a specified machine using a Docker image +name: Sync and build +description: | + This reusable workflow syncs and builds FastRPC code for a specified machine and firmware + using a Docker image. It performs code checkout, Docker image build, workspace sync, + kernel compilation, and artifact packaging. Key build outputs (Image, ramdisk, modules, DTB) + are uploaded to S3, and the runner workspace is cleaned to manage disk usage. A summary of + the build status is added to the GitHub Actions summary. on: workflow_call: @@ -31,20 +36,20 @@ jobs: fetch-depth: 1 - name: Build fastrpc docker image - uses: qualcomm/fastrpc/.github/actions/build_docker_image@main + uses: qualcomm/fastrpc/.github/actions/build_docker_image@development with: image: ${{ inputs.docker_image }} - name: Sync codebase id: sync - uses: qualcomm/fastrpc/.github/actions/sync@main + uses: qualcomm/fastrpc/.github/actions/sync@development with: machine: ${{ inputs.machine }} firmware: ${{ inputs.firmware }} - name: Build workspace id: build_workspace - uses: qualcomm/fastrpc/.github/actions/build@main + uses: qualcomm/fastrpc/.github/actions/build@development with: docker_image: ${{ inputs.docker_image }} workspace_path: ${{ steps.sync.outputs.workspace_path }} @@ -70,7 +75,7 @@ jobs: cat "$workspace/artifacts/file_list.txt" - name: Upload artifacts - uses: qualcomm/fastrpc/.github/actions/aws_s3_helper@main + uses: qualcomm/fastrpc/.github/actions/aws_s3_helper@development with: s3_bucket: qli-prd-fastrpc-gh-artifacts # local_file points to the list of files to upload diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0a9b8f0..32d5912 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,4 @@ name: test - description: | Run tests on LAVA for a specified machine using a Docker image. This reusable GitHub Actions workflow executes FastRPC tests on LAVA infrastructure. @@ -45,7 +44,7 @@ jobs: fetch-depth: 0 - name: Build fastrpc docker image - uses: qualcomm/fastrpc/.github/actions/build_docker_image@main + uses: qualcomm/fastrpc/.github/actions/build_docker_image@development with: image: ${{ inputs.docker_image }} @@ -78,7 +77,7 @@ jobs: core.setOutput("LAVANAME", lavaname); - name: Create fastrpc - lava job definition - uses: qualcomm/fastrpc/.github/actions/lava_job_render@main + uses: qualcomm/fastrpc/.github/actions/lava_job_render@development id: create_job_definition with: docker_image: ${{ inputs.docker_image }} From 2592eb4b8680792b95ee25c2ec3df152d5c09728 Mon Sep 17 00:00:00 2001 From: Ekansh Gupta Date: Thu, 4 Sep 2025 13:37:50 +0530 Subject: [PATCH 08/40] inc: drop AEEStdDef.h from rpcmem.h to fix build Fix the build failure for client where the vendor header fastrpc/rpcmem.h unconditionally includes AEEStdDef.h, which is not exported by fastrpc project. .../usr/include/fastrpc/rpcmem.h:7:10: fatal error: AEEStdDef.h: No such file or directory 7 | #include "AEEStdDef.h" | ^~~~~~~~~~~~~ compilation terminated. Signed-off-by: Ekansh Gupta --- inc/rpcmem.h | 1 - 1 file changed, 1 deletion(-) diff --git a/inc/rpcmem.h b/inc/rpcmem.h index d5d6e3a..c9e1505 100644 --- a/inc/rpcmem.h +++ b/inc/rpcmem.h @@ -4,7 +4,6 @@ #ifndef RPCMEM_H #define RPCMEM_H -#include "AEEStdDef.h" #include "stddef.h" /** From 53a2f9b90485c720260d3ece9547d7801ff826c5 Mon Sep 17 00:00:00 2001 From: Vinayak Katoch Date: Mon, 14 Jul 2025 22:02:46 +0530 Subject: [PATCH 09/40] test: Update fastrpc_test to append library paths The fastrpc_test.c now appends testlibdir and testdspdir to LD_LIBRARY_PATH and DSP_LIBRARY_PATH, respectively. Previously, these environment variables were overwritten. Makefile.am was updated to pass -Dtestlibdir and -Dtestdspdir to fastrpc_test.c, enabling correct environment variable construction. Signed-off-by: Vinayak Katoch --- test/Makefile.am | 4 +++- test/fastrpc_test.c | 43 +++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/test/Makefile.am b/test/Makefile.am index 9d37214..50d6c15 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -5,7 +5,9 @@ bin_PROGRAMS = fastrpc_test fastrpc_test_SOURCES = fastrpc_test.c # Define the compiler flags for the test program -fastrpc_test_CFLAGS = -I$(top_srcdir)/inc -DUSE_SYSLOG +fastrpc_test_CFLAGS = -I$(top_srcdir)/inc -DUSE_SYSLOG \ + -Dtestlibdir=\"$(testlibdir)\" \ + -Dtestdspdir=\"$(testdspdir)\" if ANDROID_CC USE_LOG = -llog diff --git a/test/fastrpc_test.c b/test/fastrpc_test.c index 275bcc1..89f57e2 100644 --- a/test/fastrpc_test.c +++ b/test/fastrpc_test.c @@ -39,9 +39,10 @@ int main(int argc, char *argv[]) { bool is_unsignedpd_enabled = true; // Default to unsigned PD const char *target = "linux"; // Default target platform const char *arch_version = "v68"; // Default architecture version - char abs_lib_path[PATH_MAX]; char ld_lib_path[PATH_MAX]; char dsp_lib_path[PATH_MAX]; + char *current_ld_lib_path; + char *current_dsp_lib_path; DIR *dir; struct dirent *entry; char full_lib_path[PATH_MAX]; @@ -60,6 +61,11 @@ int main(int argc, char *argv[]) { break; case 't': target = optarg; + if (strcmp(target, "linux") != 0 && strcmp(target, "android") != 0) { + printf("\nERROR: Invalid target platform (-t). Must be linux or android.\n"); + print_usage(); + return -1; + } break; case 'a': arch_version = optarg; @@ -75,44 +81,37 @@ int main(int argc, char *argv[]) { } } - // Construct the absolute library path - snprintf(abs_lib_path, sizeof(abs_lib_path), "%s", target); - - if (realpath(abs_lib_path, abs_lib_path) == NULL) { - fprintf(stderr, "Error resolving path %s: %s\n", abs_lib_path, strerror(errno)); - return -1; - } - - // Construct the absolute DSP library path - snprintf(dsp_lib_path, sizeof(dsp_lib_path), "%s", arch_version); - - if (realpath(dsp_lib_path, dsp_lib_path) == NULL) { - fprintf(stderr, "Error resolving path %s: %s\n", dsp_lib_path, strerror(errno)); - return -1; + current_ld_lib_path = getenv("LD_LIBRARY_PATH"); + if (current_ld_lib_path) { + snprintf(ld_lib_path, sizeof(ld_lib_path), "%s;%s", current_ld_lib_path, testlibdir); + } else { + snprintf(ld_lib_path, sizeof(ld_lib_path), "%s", testlibdir); } - - // Construct LD_LIBRARY_PATH and DSP_LIBRARY_PATH - snprintf(ld_lib_path, sizeof(ld_lib_path), "%s", abs_lib_path); - if (setenv("LD_LIBRARY_PATH", ld_lib_path, 1) != 0) { fprintf(stderr, "Error setting LD_LIBRARY_PATH: %s\n", strerror(errno)); return -1; } + current_dsp_lib_path = getenv("DSP_LIBRARY_PATH"); + if (current_dsp_lib_path) { + snprintf(dsp_lib_path, sizeof(dsp_lib_path), "%s;%s/%s", current_dsp_lib_path, testdspdir, arch_version); + } else { + snprintf(dsp_lib_path, sizeof(dsp_lib_path), "%s/%s", testdspdir, arch_version); + } if (setenv("DSP_LIBRARY_PATH", dsp_lib_path, 1) != 0) { fprintf(stderr, "Error setting DSP_LIBRARY_PATH: %s\n", strerror(errno)); return -1; } - dir = opendir(abs_lib_path); + dir = opendir(testlibdir); if (!dir) { - fprintf(stderr, "Error opening directory %s: %s\n", abs_lib_path, strerror(errno)); + fprintf(stderr, "Error opening directory %s: %s\n", testlibdir, strerror(errno)); return -1; } while ((entry = readdir(dir)) != NULL) { if (entry->d_type == DT_REG && strstr(entry->d_name, ".so")) { - snprintf(full_lib_path, sizeof(full_lib_path), "%s/%s", abs_lib_path, entry->d_name); + snprintf(full_lib_path, sizeof(full_lib_path), "%s/%s", testlibdir, entry->d_name); lib_handle = dlopen(full_lib_path, RTLD_LAZY); if (!lib_handle) { From 487c44c67435bb0d6db3128e286d52be3e597c66 Mon Sep 17 00:00:00 2001 From: Vinayak Katoch Date: Wed, 20 Aug 2025 16:01:36 +0530 Subject: [PATCH 10/40] test: Update fastrpc_test README for install and run Updated README.md to reflect make install for installation, replacing manual file copying. Simplified running instructions by removing the need to cd to /usr/bin, as files are now in standard system paths. Signed-off-by: Vinayak Katoch --- test/README.md | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/test/README.md b/test/README.md index 9a5c640..589102f 100644 --- a/test/README.md +++ b/test/README.md @@ -12,28 +12,20 @@ After building the application, the following files and directories need to be p 4. **v68 Directory**: Contains skeletons for the v68 architecture version. 5. **v75 Directory**: Contains skeletons for the v75 architecture version. -Copy the following files and directories to `/usr/bin` on the target device: +### Installing the Test -- `/path/to/your/fastrpc/test/.libs/fastrpc_test` -- `/path/to/your/fastrpc/test/android` (if using Android) -- `/path/to/your/fastrpc/test/linux` (if using Linux) -- `/path/to/your/fastrpc/test/v68` -- `/path/to/your/fastrpc/test/v75` +Use `make install` to install the libraries and the executable to the appropriate directories: -**Note**: Push the `android` directory if you are using the Android platform, and the `linux` directory if you are using the Linux platform. +```bash +make install +``` ## Running the Test -To run the test application, follow these steps: - -1. Navigate to the `/usr/bin` directory on the device. -2. Execute the `fastrpc_test` binary with the appropriate options. - -Example command: +To run the test application, use the following command: ```bash -cd /usr/bin -./fastrpc_test -d 3 -U 1 -t linux -a v68 +fastrpc_test -d 3 -U 1 -t linux -a v68 ``` ### Options From 9577aa7f1e627326a6bd5dbdc7f7d50e97ce0ab2 Mon Sep 17 00:00:00 2001 From: Vinayak Katoch Date: Fri, 4 Jul 2025 15:36:45 +0530 Subject: [PATCH 11/40] Reduce log verbosity to prevent syslog flooding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved all non-essential logs—such as warnings and non-user-impacting errors—to the RUNTIME level. It retains a single log each for framework initialization and deinitialization, and includes logs for PD creation and handle open/close. Signed-off-by: Vinayak Katoch --- src/apps_std_imp.c | 4 ++-- src/fastrpc_apps_user.c | 23 ++++++++++++----------- src/fastrpc_config.c | 4 ++-- src/fastrpc_context.c | 4 ++-- src/fastrpc_notif.c | 2 +- src/listener_android.c | 1 + src/mod_table.c | 2 +- 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/apps_std_imp.c b/src/apps_std_imp.c index 3abe1ae..3458d8c 100644 --- a/src/apps_std_imp.c +++ b/src/apps_std_imp.c @@ -378,7 +378,7 @@ __QAIC_IMPL(apps_std_fopen_fd)(const char *name, const char *mode, int *fd, "us, rpc_alloc:%" PRIu64 "us, mmap:%" PRIu64 "us", __func__, name, fopen_time, read_time, rpc_alloc_time, mmap_time); - FARF(CRITICAL, + FARF(RUNTIME_RPC_CRITICAL, "%s: done for %s with fopen:%" PRIu64 "us, read:%" PRIu64 "us, rpc_alloc:%" PRIu64 "us, mmap:%" PRIu64 "us, fd 0x%x error_code 0x%x", __func__, name, fopen_time, read_time, rpc_alloc_time, mmap_time, *fd, nErr); @@ -1032,7 +1032,7 @@ __QAIC_IMPL_EXPORT int __QAIC_IMPL(apps_std_fopen_with_env)( uint16_t absNameLen = 0; int domain = GET_DOMAIN_FROM_EFFEC_DOMAIN_ID(get_current_domain()); - FARF(LOW, "Entering %s", __func__); + FARF(RUNTIME_RPC_LOW, "Entering %s", __func__); VERIFYC(NULL != mode, AEE_EBADPARM); VERIFYC(NULL != delim, AEE_EBADPARM); VERIFYC(NULL != name, AEE_EBADPARM); diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index 4a79982..9da3254 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -829,12 +829,12 @@ static void print_open_handles(int domain) { struct handle_info *hi = NULL; QNode *pn = NULL; - FARF(ALWAYS, "List of open handles on domain %d:\n", domain); + FARF(RUNTIME_RPC_HIGH, "List of open handles on domain %d:\n", domain); pthread_mutex_lock(&hlist[domain].mut); QLIST_FOR_ALL(&hlist[domain].ql, pn) { hi = STD_RECOVER_REC(struct handle_info, qn, pn); if (hi->name) - FARF(ALWAYS, "%s, handle 0x%"PRIx64"", + FARF(RUNTIME_RPC_HIGH, "%s, handle 0x%"PRIx64"", hi->name, hi->remote); } pthread_mutex_unlock(&hlist[domain].mut); @@ -868,7 +868,7 @@ static char* get_lib_name(const char *uri) { } } bail: - FARF(ERROR, "Warning 0x%x: %s failed for uri %s", + FARF(RUNTIME_RPC_ERROR, "Warning 0x%x: %s failed for uri %s", nErr, __func__, uri); return NULL; } @@ -1878,7 +1878,7 @@ int remote_handle_close_domain(int domain, remote_handle h) { nErr, __func__, h, domain, dlerrstr, strerror(errno)); } } else { - FARF(ALWAYS, "%s: closed module with handle 0x%x (skel unload time %" PRIu64 " us)", + FARF(RUNTIME_RPC_HIGH, "%s: closed module with handle 0x%x (skel unload time %" PRIu64 " us)", __func__, h, t_close); } if (dlerrstr) { @@ -2848,7 +2848,7 @@ int remote_session_control(uint32_t req, void *data, uint32_t datalen) { VERIFY(AEE_SUCCESS == (nErr = fastrpc_notif_register(ii, notif))); } } - FARF(ALWAYS, "%s Register PD status notification request for domain %d\n", + FARF(RUNTIME_RPC_HIGH, "%s Register PD status notification request for domain %d\n", __func__, domain); break; } @@ -3166,7 +3166,7 @@ static void domain_deinit(int domain) { return; } olddev = hlist[domain].dev; - FARF(ALWAYS, "%s for domain %d: dev %d", __func__, domain, olddev); + FARF(RUNTIME_RPC_HIGH, "%s for domain %d: dev %d", __func__, domain, olddev); if (olddev != -1) { FASTRPC_ATRACE_BEGIN_L("%s called for handle 0x%x, domain %d, dev %d", @@ -3215,8 +3215,9 @@ static void domain_deinit(int domain) { memset(hlist[domain].dsppdname, 0, MAX_DSPPD_NAMELEN); memset(hlist[domain].sessionname, 0, MAX_DSPPD_NAMELEN); PROFILE_ALWAYS(&t_kill, close_device_node(domain, olddev);); - FARF(ALWAYS, "%s: closed device %d on domain %d (kill time %" PRIu64 " us)", + FARF(RUNTIME_RPC_HIGH, "%s: closed device %d on domain %d (kill time %" PRIu64 " us)", __func__, olddev, domain, t_kill); + FARF(ALWAYS, "%s done for domain %d.", __func__, domain); FASTRPC_ATRACE_END(); } hlist[domain].proc_sharedbuf_cur_addr = NULL; @@ -3475,7 +3476,7 @@ static int open_shell(int domain_id, apps_std_FILE *fh, int unsigned_shell) { } } if (!nErr) - FARF(ALWAYS, "Successfully opened %s, domain %d", absName, domain); + FARF(RUNTIME_RPC_HIGH, "Successfully opened %s, domain %d", absName, domain); bail: if (domain_str) { free(domain_str); @@ -3535,7 +3536,7 @@ static int fastrpc_enable_kernel_optimizations(int domain) { } bail: if (nErr) { - FARF(ERROR, "Error 0x%x: %s failed for domain %d (%s)\n", nErr, __func__, + FARF(RUNTIME_RPC_ERROR, "Error 0x%x: %s failed for domain %d (%s)\n", nErr, __func__, domain, strerror(errno)); } /* @@ -3584,7 +3585,7 @@ void print_process_attrs(int domain) { if (fastrpc_config_is_logpacket_enabled()) logpkt = true; FARF(ALWAYS, - "Created user PD on domain %d, dbg_trace 0x%x, enabled attr=> RPC " + "Info: Created user PD on domain %d, dbg_trace 0x%x, enabled attr=> RPC " "timeout:%d, Dbg Mode:%s, CRC:%s, Unsigned:%s, Signed:%s, Adapt QOS:%s, " "PD dump: (Config:%s, Dbg:%s), Perf: (Kernel:%s, DSP:%s), Iregion:%s, " "QTF:%s, UAF:%s userPD initmem len:0x%x, Log pkt: %s", @@ -3832,7 +3833,7 @@ static int remote_init(int domain) { __attribute__((destructor)) static void close_dev(void) { int i; - FARF(ALWAYS, "%s: unloading library %s", __func__, + FARF(RUNTIME_RPC_HIGH, "%s: unloading library %s", __func__, fastrpc_library[DEFAULT_DOMAIN_ID]); FOR_EACH_EFFECTIVE_DOMAIN_ID(i) { domain_deinit(i); diff --git a/src/fastrpc_config.c b/src/fastrpc_config.c index 7629dd0..7073aa8 100644 --- a/src/fastrpc_config.c +++ b/src/fastrpc_config.c @@ -378,7 +378,7 @@ int fastrpc_config_init() { VERIFYC(NULL != (config_file = calloc(1, sizeof(char) * len)), AEE_ENOMEMORY); // Prepare config filename snprintf(config_file, len, "%s%s", name, file_extension); - FARF(ALWAYS, "Reading configuration file: %s\n", config_file); + FARF(RUNTIME_RPC_HIGH, "Reading configuration file: %s\n", config_file); // Get the required size for PATH apps_std_get_search_paths_with_env(ADSP_LIBRARY_PATH, ";", NULL, 0, &numPaths, @@ -416,7 +416,7 @@ int fastrpc_config_init() { } } if (!file_found) { - FARF(ALWAYS, "%s: Couldn't find file %s, errno (%s) at %s\n", __func__, + FARF(RUNTIME_RPC_HIGH, "%s: Couldn't find file %s, errno (%s) at %s\n", __func__, config_file, strerror(errno), data_paths); } bail: diff --git a/src/fastrpc_context.c b/src/fastrpc_context.c index 0dfa038..4e32a04 100644 --- a/src/fastrpc_context.c +++ b/src/fastrpc_context.c @@ -296,7 +296,7 @@ int fastrpc_context_table_init(void) { if (nErr) FARF(ERROR, "Error 0x%x: %s failed\n", nErr, __func__); else - FARF(ALWAYS, "%s done", __func__); + FARF(RUNTIME_RPC_HIGH, "%s done", __func__); return nErr; } @@ -322,7 +322,7 @@ int fastrpc_context_table_deinit(void) { if (nErr) FARF(ERROR, "Error 0x%x: %s failed\n", nErr, __func__); else - FARF(ALWAYS, "%s done", __func__); + FARF(RUNTIME_RPC_HIGH, "%s done", __func__); return nErr; } \ No newline at end of file diff --git a/src/fastrpc_notif.c b/src/fastrpc_notif.c index 1da48c3..be4bde6 100644 --- a/src/fastrpc_notif.c +++ b/src/fastrpc_notif.c @@ -101,7 +101,7 @@ void fastrpc_notif_domain_deinit(int domain) { GET_HASH_NODE(notif_config, domain, me); if (!me) { - FARF(ALWAYS, "Warning: %s: unable to find hash-node for domain %d", + FARF(RUNTIME_RPC_HIGH, "Warning: %s: unable to find hash-node for domain %d", __func__, domain); return; } diff --git a/src/listener_android.c b/src/listener_android.c index a190ab0..712945c 100644 --- a/src/listener_android.c +++ b/src/listener_android.c @@ -321,6 +321,7 @@ static void listener(listener_config *me) { "Error 0x%x : Writing to listener event_fd %d failed (errno %s)", nErr, me->eventfd, strerror(errno)); } + FARF(ALWAYS, "%s thread exiting\n", __func__); dlerror(); } diff --git a/src/mod_table.c b/src/mod_table.c index 720d5a2..d19fa90 100644 --- a/src/mod_table.c +++ b/src/mod_table.c @@ -217,7 +217,7 @@ static void open_mod_table_dtor_imp(void *data) { if (dm->dlhandle) { DLCLOSE(dm->dlhandle); } - FARF(ALWAYS, "%s: closed reverse module %s with handle 0x%x", __func__, + FARF(RUNTIME_RPC_HIGH, "%s: closed reverse module %s with handle 0x%x", __func__, dm->uri, (uint32_t)dm->key); dm->key = 0; } From aa08ed9ef0def2144e6165daf2d0058d7ffbc167 Mon Sep 17 00:00:00 2001 From: Vinayak Katoch Date: Fri, 8 Aug 2025 00:11:19 +0530 Subject: [PATCH 12/40] Remove PRINT_WARN_USE_DOMAINS macro completely The PRINT_WARN_USE_DOMAINS() macro, along with its conditional Android-specific warning about non-domain usage of FastRPC, has been entirely removed from the codebase. Signed-off-by: Vinayak Katoch --- inc/fastrpc_internal.h | 7 ------- src/dspqueue/dspqueue_cpu.c | 1 - src/fastrpc_apps_user.c | 5 ----- src/fastrpc_mem.c | 6 ------ 4 files changed, 19 deletions(-) diff --git a/inc/fastrpc_internal.h b/inc/fastrpc_internal.h index ca34f0a..859bfe8 100644 --- a/inc/fastrpc_internal.h +++ b/inc/fastrpc_internal.h @@ -471,13 +471,6 @@ static __inline int convert_kernel_to_user_error(int nErr, int err_no) { return nErr; } -#ifdef __ANDROID__ -// Warning message to use domains -#define PRINT_WARN_USE_DOMAINS() VERIFY_WPRINTF("Warning: %s: Non-domain usage of FastRPC will be deprecated, use domains to offload to DSP using FastRPC", __func__) -#else -#define PRINT_WARN_USE_DOMAINS() /* empty */ -#endif - /** * @brief utility APIs used in fastRPC library to get name, handle from domain **/ diff --git a/src/dspqueue/dspqueue_cpu.c b/src/dspqueue/dspqueue_cpu.c index 9d92b9a..66092fa 100644 --- a/src/dspqueue/dspqueue_cpu.c +++ b/src/dspqueue/dspqueue_cpu.c @@ -462,7 +462,6 @@ AEEResult dspqueue_create(int domain, uint32_t flags, uint32_t req_queue_size, errno = 0; if (domain == -1) { - PRINT_WARN_USE_DOMAINS(); domain = get_current_domain(); if (!IS_VALID_EFFECTIVE_DOMAIN_ID(domain)) { return AEE_ERPC; diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index 9da3254..5b4ceb2 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -1460,7 +1460,6 @@ int remote_handle_invoke(remote_handle handle, uint32_t sc, remote_arg *pra) { FARF(RUNTIME_RPC_HIGH, "Entering %s, handle %u sc %X remote_arg %p\n", __func__, handle, sc, pra); - PRINT_WARN_USE_DOMAINS(); FASTRPC_ATRACE_BEGIN_L("%s called with handle 0x%x , scalar 0x%x", __func__, (int)handle, sc); VERIFYC(handle != (remote_handle)-1, AEE_EINVHANDLE); @@ -1546,7 +1545,6 @@ int remote_handle_invoke_async(remote_handle handle, FARF(RUNTIME_RPC_HIGH, "Entering %s, handle %u desc %p sc %X remote_arg %p\n", __func__, handle, desc, sc, pra); - PRINT_WARN_USE_DOMAINS(); FASTRPC_ATRACE_BEGIN_L("%s called with handle 0x%x , scalar 0x%x", __func__, (int)handle, sc); VERIFYC(handle != (remote_handle)-1, AEE_EINVHANDLE); @@ -1749,7 +1747,6 @@ int remote_handle_open(const char *name, remote_handle *ph) { VERIFY(AEE_SUCCESS == (nErr = fastrpc_init_once())); FARF(RUNTIME_RPC_HIGH, "Entering %s, name %s\n", __func__, name); - PRINT_WARN_USE_DOMAINS(); FASTRPC_ATRACE_BEGIN_L("%s for %s", __func__, name); if (!name || !ph) { @@ -1896,7 +1893,6 @@ int remote_handle_close(remote_handle h) { FARF(RUNTIME_RPC_HIGH, "Entering %s, handle %lu\n", __func__, h); - PRINT_WARN_USE_DOMAINS(); VERIFY(AEE_SUCCESS == (nErr = remote_handle_close_domain(domain, h))); FASTRPC_PUT_REF(domain); fastrpc_update_module_list(NON_DOMAIN_LIST_DEQUEUE, domain, h, NULL, NULL); @@ -2404,7 +2400,6 @@ int remote_handle_control(uint32_t req, void *data, uint32_t len) { FARF(RUNTIME_HIGH, "Entering %s, req %d, data %p, size %d\n", __func__, req, data, len); - PRINT_WARN_USE_DOMAINS(); domain = get_current_domain(); FASTRPC_GET_REF(domain); diff --git a/src/fastrpc_mem.c b/src/fastrpc_mem.c index bd0e79a..8f62fc2 100644 --- a/src/fastrpc_mem.c +++ b/src/fastrpc_mem.c @@ -469,7 +469,6 @@ int fastrpc_mmap(int domain, int fd, void *vaddr, int offset, size_t length, // Get domain and open session if not already open if (domain == -1) { - PRINT_WARN_USE_DOMAINS(); domain = get_current_domain(); } VERIFYC(IS_VALID_EFFECTIVE_DOMAIN_ID(domain), AEE_EBADPARM); @@ -551,7 +550,6 @@ int fastrpc_munmap(int domain, int fd, void *vaddr, size_t length) { FARF(RUNTIME_RPC_HIGH, "%s: domain %d fd %d vaddr %p length 0x%zx", __func__, domain, fd, vaddr, length); if (domain == -1) { - PRINT_WARN_USE_DOMAINS(); domain = get_current_domain(); } VERIFYC(fd >= 0 && IS_VALID_EFFECTIVE_DOMAIN_ID(domain), @@ -633,7 +631,6 @@ int remote_mem_map(int domain, int fd, int flags, uint64_t vaddr, size_t size, VERIFYC(flags >= 0 && flags < REMOTE_MAP_MAX_FLAG && raddr != NULL, AEE_EBADPARM); if (domain == -1) { - PRINT_WARN_USE_DOMAINS(); domain = get_current_domain(); } VERIFYC(IS_VALID_EFFECTIVE_DOMAIN_ID(domain), AEE_EBADPARM); @@ -667,7 +664,6 @@ int remote_mem_unmap(int domain, uint64_t raddr, size_t size) { FARF(RUNTIME_RPC_HIGH, "%s: domain %d addr 0x%llx size 0x%zx", __func__, domain, raddr, size); if (domain == -1) { - PRINT_WARN_USE_DOMAINS(); domain = get_current_domain(); } VERIFYC(IS_VALID_EFFECTIVE_DOMAIN_ID(domain), AEE_EBADPARM); @@ -722,7 +718,6 @@ int remote_mmap64(int fd, uint32_t flags, uint64_t vaddrin, int64_t size, VERIFY(AEE_SUCCESS == (nErr = fastrpc_init_once())); - PRINT_WARN_USE_DOMAINS(); if (flags != 0) { nErr = AEE_EBADPARM; goto bail; @@ -781,7 +776,6 @@ int remote_munmap64(uint64_t vaddrout, int64_t size) { } int remote_munmap(uint32_t vaddrout, int size) { - PRINT_WARN_USE_DOMAINS(); return remote_munmap64((uintptr_t)vaddrout, (int64_t)size); } From 2d1768e9d602641660aa2232c96577042c1b6294 Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Tue, 9 Sep 2025 12:04:28 +0530 Subject: [PATCH 13/40] Fix ramdisk unpack and repack logic for fastrpc validation (#238) * CI: Switch all reusable workflow references to 'development' branch - Ensures CI uses latest logic from development branch Signed-off-by: Tharun Kumar Merugu * CI Fix: Ensure proper packaging of ramdisk contents into cpio archive - Added missing directory creation for ramdisk_test before decompression - Ensured only contents of ramdisk_fastrpc are archived (not the folder itself) - Applied safe and consistent cpio and gzip usage - Improved error handling and quoting for robustness Signed-off-by: Tharun Kumar Merugu --------- Signed-off-by: Tharun Kumar Merugu --- .github/actions/build/action.yml | 11 +++++------ .github/actions/loading/action.yml | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 6b90942..ac32a00 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -87,9 +87,7 @@ runs: cp -rf src/adsprpcd src/cdsprpcd src/sdsprpcd ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin/ echo "Copy fastrpc test files" - cp -rf test/fastrpc_test ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin - cp -rf test/linux/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin - cp -rf test/v75/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin + cp -rf test/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin echo "Package FastRPC firmware files" cp -r ${{ inputs.workspace_path }}/firmware_dir/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/ @@ -100,7 +98,9 @@ runs: ls -la ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ cd ${{ inputs.workspace_path }}/artifacts/ - find ramdisk_fastrpc | cpio -o -H newc > ramdisk_fastrpc.cpio + cd ramdisk_fastrpc + find . | cpio -o -H newc > ../ramdisk_fastrpc.cpio + cd .. gzip -9 ramdisk_fastrpc.cpio mv ramdisk_fastrpc.cpio.gz ramdisk_fastrpc.gz @@ -109,8 +109,7 @@ runs: run: | echo "Unpack and mount ramdisk" cd ${{ inputs.workspace_path }}/artifacts - - # Decompress the ramdisk.gz safely + mkdir -p ramdisk_test gunzip -c ramdisk_fastrpc.gz > ramdisk_test/ramdisk echo "Unpacking ramdisk" diff --git a/.github/actions/loading/action.yml b/.github/actions/loading/action.yml index c97910f..07d23c5 100644 --- a/.github/actions/loading/action.yml +++ b/.github/actions/loading/action.yml @@ -39,12 +39,12 @@ runs: core.setFailed(`Failed to load or parse MACHINES.json: ${err.message}`); return; } - # Slim matrix for better job visibility + // Slim matrix for better job visibility const slim_matrix = Object.entries(file).map(([machine, [firmware]]) => ({ machine, firmware })); core.setOutput('build_matrix', JSON.stringify(slim_matrix)); console.log(slim_matrix); - # Full matrix to pass to test jobs + // Full matrix to pass to test jobs const complete_matrix = Object.entries(file).map(([machine, [firmware, lavaname]]) => ({ machine, firmware, From ec7bf169558713b1f2eca145e7b8369ff39d8a73 Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Fri, 12 Sep 2025 17:02:06 +0530 Subject: [PATCH 14/40] CI: Automate fastrpc deployment Integrates `make install` with `DESTDIR=${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc` to automate the deployment of fastrpc bins, libs, and tests. This change directly addresses upstream requirements (https://github.com/qualcomm/fastrpc/pull/194) for specific stub/skeleton placement. It eliminates manual copying, ensuring proper installation paths essential for `LD_LIBRARY_PATH` and `DSP_LIBRARY_PATH` configuration. Resolves intermittent GitHub Actions LAVA test failures caused by incorrect library setup. Signed-off-by: Tharun Kumar Merugu --- .github/actions/build/action.yml | 36 +------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index ac32a00..cae6e2f 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -31,26 +31,10 @@ runs: -v "$(dirname $PWD)":"$(dirname $PWD)" \ ${{ inputs.docker_image }} bash -c " ./gitcompile --host=aarch64-linux-gnu + make install DESTDIR=${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc " echo "::endgroup::" - echo "Verify the compiled fastrpc files" - Files=( - src/.libs/libadsp_default_listener.so - src/.libs/libadsprpc.so - src/.libs/libcdsp_default_listener.so - src/.libs/libcdsprpc.so - src/.libs/libsdsp_default_listener.so - src/.libs/libsdsprpc.so - src/adsprpcd - src/cdsprpcd - src/sdsprpcd - ) - for File in "${Files[@]}" - do - if [ -f "$File" ] ; then echo "$File - Exists" ; else echo "$File - Not Exists" && exit 1 ; fi - done - - name: Build kernel using Docker Image shell: bash run: | @@ -71,24 +55,6 @@ runs: - name: Package Files into ramdisk shell: bash run: | - # Create directories for pushing fastrpc binaries - mkdir -p ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib - mkdir -p ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin - - cd ${{ inputs.workspace_path }} - - echo "Copy fastrpc files" - cp -rf src/.libs/libadsp_default_listener.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ - cp -rf src/.libs/libadsprpc.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ - cp -rf src/.libs/libcdsp_default_listener.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ - cp -rf src/.libs/libcdsprpc.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ - cp -rf src/.libs/libsdsp_default_listener.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ - cp -rf src/.libs/libsdsprpc.so* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/lib/ - cp -rf src/adsprpcd src/cdsprpcd src/sdsprpcd ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin/ - - echo "Copy fastrpc test files" - cp -rf test/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/usr/bin - echo "Package FastRPC firmware files" cp -r ${{ inputs.workspace_path }}/firmware_dir/* ${{ inputs.workspace_path }}/artifacts/ramdisk_fastrpc/ From 939b2a19b6a4ec42248242c99c640f24fc1ffc55 Mon Sep 17 00:00:00 2001 From: Abhinav Parihar Date: Tue, 16 Sep 2025 13:59:55 +0530 Subject: [PATCH 15/40] Enable PD exception logging by default Remove dependency on the PD_EXCEPTION_LOGGING compile-time flag to ensure PD exception logging is enabled by default. This change allows DSP fatal logs to be redirected to the shared buffer for apps-side logging without requiring explicit flag definition. Signed-off-by: Abhinav Parihar --- src/fastrpc_apps_user.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index 81fef0a..6de574a 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -4040,7 +4040,6 @@ static int domain_init(int domain, int *dev) { ret == (int)(DSP_AEE_EOFFSET + AEE_EUNSUPPORTED), ret); } -#ifdef PD_EXCEPTION_LOGGING if ((dom != SDSP_DOMAIN_ID) && hlist[domain].dsppd == ROOT_PD) { remote_handle64 handle = 0; handle = get_adspmsgd_adsp1_handle(domain); @@ -4048,7 +4047,6 @@ static int domain_init(int domain, int *dev) { adspmsgd_init(handle, 0x10); // enable PD exception logging } } -#endif fastrpc_perf_init(hlist[domain].dev, domain); VERIFY(AEE_SUCCESS == (nErr = fastrpc_latency_init(hlist[domain].dev, &hlist[domain].qos))); From 8c54dc7171fab22a333bc8d6bd8c0d271d15e15f Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Tue, 14 Oct 2025 16:08:41 +0530 Subject: [PATCH 16/40] Matrix Consolidation & Simplified LAVA Machine Assignment - Removed separate `full_matrix` definition. - Consolidated all configuration under a single `build_matrix`. - Included fields: `machine`, `firmware` and `lavaname`. - Removed the logic for extracting the LAVA machine name dynamically. - The machine name is now directly assigned from build_matrix.lavaname, reducing complexity and improving maintainability. Signed-off-by: Tharun Kumar Merugu --- .github/actions/loading/action.yml | 44 ++++++++++++++---------------- .github/workflows/loading.yml | 6 +--- .github/workflows/pre_merge.yml | 11 ++++---- .github/workflows/test.yml | 27 ++---------------- ci/MACHINES.json | 14 ++++++++-- 5 files changed, 39 insertions(+), 63 deletions(-) diff --git a/.github/actions/loading/action.yml b/.github/actions/loading/action.yml index 07d23c5..101d3c2 100644 --- a/.github/actions/loading/action.yml +++ b/.github/actions/loading/action.yml @@ -2,21 +2,14 @@ name: Load Parameters description: | This composite action loads build parameters from a `MACHINES.json` file. It parses the JSON file to create two matrices: - - `build_matrix`: A slimmed-down matrix containing only 'machine' and 'firmware' for better visibility in job names. - - `full_matrix`: A complete matrix including 'machine', 'firmware', and 'lavaname' for passing all necessary details to test jobs. + - `build_matrix`: A complete matrix containing 'machine', 'firmware' and 'lavaname' It expects the `MACHINES.json` file to be located at `ci/MACHINES.json` relative to the workspace root. - Each entry in `MACHINES.json` should be a key-value pair where the key is the machine name - and the value is an array `[firmware, lavaname]`. outputs: build_matrix: description: Build matrix value: ${{ steps.set-matrix.outputs.build_matrix }} - full_matrix: - description: full matrix containing lava devails - value: ${{ steps.set-matrix.outputs.full_matrix }} - runs: using: "composite" steps: @@ -28,27 +21,30 @@ runs: const fs = require('fs'); const path = require('path'); const filePath = path.join(process.env.GITHUB_WORKSPACE, 'ci', 'MACHINES.json'); - let file; + let fileData; try { if (!fs.existsSync(filePath)) { - core.setFailed(`MACHINES.json not found at ${filePath}`); + core.setFailed(`Error: MACHINES.json not found at ${filePath}`); return; } - file = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + fileData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch (err) { - core.setFailed(`Failed to load or parse MACHINES.json: ${err.message}`); + core.setFailed(`Error loading or parsing MACHINES.json: ${err.message}`); return; } - // Slim matrix for better job visibility - const slim_matrix = Object.entries(file).map(([machine, [firmware]]) => ({ machine, firmware })); - core.setOutput('build_matrix', JSON.stringify(slim_matrix)); - console.log(slim_matrix); - // Full matrix to pass to test jobs - const complete_matrix = Object.entries(file).map(([machine, [firmware, lavaname]]) => ({ - machine, - firmware, - lavaname - })); - core.setOutput('full_matrix', JSON.stringify(complete_matrix)); - console.log(complete_matrix); + // fileData is an object, not an array + const matrix = Object.values(fileData) + .filter(item => + typeof item === 'object' && item !== null && + 'machine' in item && 'firmware' in item && 'lavaname' in item + ) + .map(({ machine, firmware, lavaname }) => ({ + machine, + firmware, + lavaname + })); + + core.setOutput('build_matrix', JSON.stringify(matrix)); + console.log("Generated build_matrix:"); + console.log(matrix); diff --git a/.github/workflows/loading.yml b/.github/workflows/loading.yml index 952c9ac..c945c48 100644 --- a/.github/workflows/loading.yml +++ b/.github/workflows/loading.yml @@ -8,16 +8,12 @@ on: description: Build matrix value: ${{ jobs.loading.outputs.build_matrix }} - full_matrix: - description: Full Matrix containing lava description - value: ${{ jobs.loading.outputs.full_matrix }} - jobs: loading: runs-on: ubuntu-latest outputs: build_matrix: ${{ steps.loading.outputs.build_matrix }} - full_matrix: ${{ steps.loading.outputs.full_matrix }} + steps: - name: Checkout Code uses: actions/checkout@v4 diff --git a/.github/workflows/pre_merge.yml b/.github/workflows/pre_merge.yml index 6ba6471..68b9411 100644 --- a/.github/workflows/pre_merge.yml +++ b/.github/workflows/pre_merge.yml @@ -5,7 +5,7 @@ description: | on pull request events (opened, synchronize, reopened) targeting 'main' or 'development', or manually via workflow_dispatch. Loading: Loads the build matrix from MACHINES.json. - Build: Syncs and Builds code for each machine/firmware pair using sync_build.yml. + Build: Syncs and Builds code for each target using sync_build.yml. Test: Runs LAVA tests using built artifacts via test.yml. All jobs use reusable workflows and inherit secrets from the caller. @@ -28,7 +28,6 @@ jobs: uses: qualcomm/fastrpc/.github/workflows/loading.yml@development secrets: inherit - # Each job in build and test will run for every {machine, firmware} pair # The build job will run on the machines specified in the build_matrix output # from the loading job. It will use the build.yml workflow to build the code. build: @@ -40,10 +39,11 @@ jobs: secrets: inherit with: docker_image: fastrpc-image:latest - machine: ${{ matrix.machine }} - firmware: ${{ matrix.firmware }} + deviceTree: ${{ matrix.deviceTree }} + linuxFirmware: ${{ matrix.linuxFirmware }} + dspHexBinary: ${{ matrix.dspHexBinary }} - # The test job will run on the machines specified in the full_matrix output + # The test job will run on the machines specified in the build_matrix output # from the loading job. It will use the test.yml workflow to run tests. test: needs: [loading, build] @@ -52,4 +52,3 @@ jobs: with: docker_image: fastrpc-image:latest build_matrix: ${{ needs.loading.outputs.build_matrix }} - full_matrix: ${{ needs.loading.outputs.full_matrix }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 32d5912..d229ed6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: test description: | - Run tests on LAVA for a specified machine using a Docker image. + Run tests on LAVA for a specified target using a Docker image. This reusable GitHub Actions workflow executes FastRPC tests on LAVA infrastructure. It leverages a Docker image for test execution and supports multi-target builds via a matrix strategy. The workflow automates job creation, submission, and result validation on LAVA, @@ -22,11 +22,6 @@ on: type: string required: true - full_matrix: - description: Full matrix containing lava description - type: string - required: true - jobs: test: runs-on: @@ -58,24 +53,6 @@ jobs: - name: Clone lava job render scripts run: cd .. && git clone https://github.com/qualcomm-linux/job_render - - name: Extract the LAVA machine name - id: get_lavaname - uses: actions/github-script@v7 - with: - script: | - const fullMatrix = JSON.parse(`${{ inputs.full_matrix }}`); - const currentMachine = `${{ matrix.build_matrix.machine }}`; - - const entry = fullMatrix.find(item => item.machine === currentMachine); - if (!entry) { - core.setFailed(`No entry found in full matrix for machine: ${currentMachine}`); - return; - } - - const lavaname = entry.lavaname; - console.log(`Lavaname for ${currentMachine} is ${lavaname}`); - core.setOutput("LAVANAME", lavaname); - - name: Create fastrpc - lava job definition uses: qualcomm/fastrpc/.github/actions/lava_job_render@development id: create_job_definition @@ -84,7 +61,7 @@ jobs: env: FIRMWARE: ${{ matrix.build_matrix.firmware }} MACHINE: ${{ matrix.build_matrix.machine }} - LAVA_NAME: ${{ steps.get_lavaname.outputs.LAVANAME }} + LAVA_NAME: ${{ matrix.build_matrix.lavaname }} - name: Submit lava job id: submit_job diff --git a/ci/MACHINES.json b/ci/MACHINES.json index 7ef482c..05435ca 100644 --- a/ci/MACHINES.json +++ b/ci/MACHINES.json @@ -1,4 +1,12 @@ { - "qcs9100-ride-r3": ["sa8775p-ride", "qcs9100-ride"], - "qcs8300-ride": ["qcs8300-ride", "qcs8300-ride"] -} + "qcs9100-ride-r3": { + "machine": "qcs9100-ride-r3", + "firmware": "sa8775p-ride", + "lavaname": "qcs9100-ride" + }, + "qcs8300-ride": { + "machine": "qcs8300-ride", + "firmware": "qcs8300-ride", + "lavaname": "qcs8300-ride" + } +} \ No newline at end of file From 3a5185adecfbd8f87644e1078d27e617b2274d67 Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Wed, 15 Oct 2025 19:35:50 +0530 Subject: [PATCH 17/40] CI Update: Refactored pull_request_target Workflow - Updated the `pull_request_target` GitHub Actions workflow. - Ensures correct context and permissions for PR-triggered jobs. - Improves security and reliability for workflows triggered by external contributions. Signed-off-by: Tharun Kumar Merugu --- .github/workflows/pre_merge.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre_merge.yml b/.github/workflows/pre_merge.yml index 6ba6471..e3736c8 100644 --- a/.github/workflows/pre_merge.yml +++ b/.github/workflows/pre_merge.yml @@ -14,12 +14,13 @@ on: branches: - 'main' - 'development' - pull_request: + workflow_dispatch: + pull_request_target: # requiring access to base repo types: [opened, synchronize, reopened] branches: - 'main' - 'development' - workflow_dispatch: + jobs: From 1f0f38aadd828f58c31f2e10b42e3844e451fdd5 Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Wed, 15 Oct 2025 19:36:44 +0530 Subject: [PATCH 18/40] CI Optimization: Ignore .github Directory in Workflow Trigger - Updated `paths-ignore` in GitHub Actions to exclude changes under `.github/**`. - Prevents unnecessary workflow runs for CI configuration-only changes. - Improves efficiency and reduces redundant CI executions. Signed-off-by: Tharun Kumar Merugu --- .github/workflows/pre_merge.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pre_merge.yml b/.github/workflows/pre_merge.yml index e3736c8..6c35aa7 100644 --- a/.github/workflows/pre_merge.yml +++ b/.github/workflows/pre_merge.yml @@ -20,7 +20,8 @@ on: branches: - 'main' - 'development' - + paths-ignore: + - '.github/**' jobs: From efc28a595ba0ac7f0c79ee3208bd1421dd58db28 Mon Sep 17 00:00:00 2001 From: Vinayak Katoch Date: Wed, 15 Oct 2025 14:37:58 +0530 Subject: [PATCH 19/40] Add GitHub Actions workflow for ABI/API compatibility check Implement automated ABI/API compatibility checks on pull requests targeting development and main branches. Build AArch64 libraries using GNU cross-compilation toolchain, generate ABI dumps with abi-dumper for libadsprpc/libcdsprpc/libsdsprpc, and compare against baseline using abi-compliance-checker. Fail workflow on incompatibilities and upload detailed HTML reports as artifacts. Signed-off-by: Vinayak Katoch --- .github/workflows/abi-compat.yml | 120 +++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/abi-compat.yml diff --git a/.github/workflows/abi-compat.yml b/.github/workflows/abi-compat.yml new file mode 100644 index 0000000..60e3c0a --- /dev/null +++ b/.github/workflows/abi-compat.yml @@ -0,0 +1,120 @@ +# ============================================================================= +# ABI/API Compatibility Check Workflow +# ============================================================================= +# Ensures that PR changes don't break binary compatibility for existing apps +# by comparing shared library ABIs between PR and development branch versions +name: ABI/API Compatibility Check + +on: + pull_request: + branches: [development, main] + +# Cross-compilation environment for AArch64 (ARM64) architecture +env: + CC: aarch64-linux-gnu-gcc + CXX: aarch64-linux-gnu-g++ + AS: aarch64-linux-gnu-as + LD: aarch64-linux-gnu-ld + RANLIB: aarch64-linux-gnu-ranlib + STRIP: aarch64-linux-gnu-strip + + # -fno-eliminate-unused-debug-types prevents GCC from optimizing away debug info + # that abi-dumper needs to extract complete type information from shared libraries + CFLAGS: "-g -Og -fno-eliminate-unused-debug-types" + CXXFLAGS: "-g -Og -fno-eliminate-unused-debug-types" + +jobs: + abi: + runs-on: ubuntu-latest + steps: + # ------------------------------------------------------------------------- + # Setup Phase: Prepare environment and install dependencies + # ------------------------------------------------------------------------- + - name: Checkout PR (new) + uses: actions/checkout@v4 + with: + # fetch-depth: 0 required for git worktree to access development branch + # without full history, worktree creation will fail + fetch-depth: 0 + + - name: Install ABI tools & build dependencies + run: | + sudo apt-get update + sudo apt-get install -y abi-compliance-checker abi-dumper automake g++-aarch64-linux-gnu binutils-aarch64-linux-gnu + abi-compliance-checker -version + abi-dumper -version + + # ------------------------------------------------------------------------- + # Build Phase: Compile both PR and baseline versions + # ------------------------------------------------------------------------- + - name: Build PR (AArch64, with debug info) + run: | + ./gitcompile --host=aarch64-linux-gnu + ls -l src/.libs || true + + - name: Prepare baseline worktree (development) + run: | + git fetch origin development:refs/remotes/origin/development + git worktree add ../baseline origin/development + + - name: Build baseline (AArch64, with debug info) + working-directory: ../baseline + run: | + ./gitcompile --host=aarch64-linux-gnu + ls -l src/.libs || true + # ------------------------------------------------------------------------- + # Analysis Phase: Compare ABI compatibility between versions + # ------------------------------------------------------------------------- + - name: Run ABI compatibility check + shell: bash + run: | + # Setup directory structure for analysis artifacts + mkdir -p abi/{dumps,reports,headers-{baseline,pr}} + + # Headers define the public API surface - abi-dumper uses these to filter + # which symbols are considered "public" vs internal implementation details + cp ../baseline/inc/{remote,rpcmem}.h abi/headers-baseline/ + cp inc/{remote,rpcmem}.h abi/headers-pr/ + + # These are the user-facing shared libraries - internal/test libraries excluded + # to focus ABI checking on what external applications actually link against + LIBS=(libadsprpc.so libcdsprpc.so libsdsprpc.so) + FAIL=0 + + # Process each library for ABI compatibility + for lib in "${LIBS[@]}"; do + base_lib="../baseline/src/.libs/$lib" + pr_lib="src/.libs/$lib" + # Extract the base name of a shared library file, excluding its .so* version suffix + name="${lib%%.so*}" + + if [[ -f "$base_lib" && -f "$pr_lib" ]]; then + echo "::group::Processing $lib" + + # Generate ABI dumps for both versions + abi-dumper "$base_lib" -o "abi/dumps/${name}.base.dump" -lver base -public-headers "abi/headers-baseline" || FAIL=1 + abi-dumper "$pr_lib" -o "abi/dumps/${name}.pr.dump" -lver pr -public-headers "abi/headers-pr" || FAIL=1 + + # Compare ABIs and generate compatibility report + abi-compliance-checker -l "$name" -old "abi/dumps/${name}.base.dump" -new "abi/dumps/${name}.pr.dump" -report-path "abi/reports/${name}.html" || FAIL=1 + + echo "::endgroup::" + else + echo "Skipping $lib (missing files)" + fi + done + + # Fail the job only after processing all libraries to get complete picture + [[ $FAIL -eq 0 ]] || { echo "ABI check failed"; exit 1; } + + # ------------------------------------------------------------------------- + # Reporting Phase: Upload analysis results for review + # ------------------------------------------------------------------------- + - name: Upload ABI reports + # if: always() ensures reports are uploaded even when ABI check fails + # - critical for debugging what specific ABI changes caused the failure + if: always() + uses: actions/upload-artifact@v4 + with: + name: abi-compat-reports + path: abi/reports From fc10de32f2fe67c491c4f26bd451a5826aaf69fa Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Tue, 14 Oct 2025 16:30:12 +0530 Subject: [PATCH 20/40] Target-Specific Enhancements: qcs6490 Support & Path Resolution - Added support for the `qcs6490` target alongside existing `qcs9100` and `qcs8300` targets. - Introduced a new `target` attribute For `qcs6490`, resolved DSP firmware and repo path discrepancy: - DSP firmware search path uses `qcs6490` - Repo folder path uses `qcm6490` - Enhances target flexibility and ensures accurate path resolution for build and test workflows. Signed-off-by: Tharun Kumar Merugu --- .github/actions/loading/action.yml | 9 +++++---- .github/actions/sync/action.yml | 15 +++++++++------ .github/workflows/sync_build.yml | 5 +++++ ci/MACHINES.json | 12 ++++++++++-- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.github/actions/loading/action.yml b/.github/actions/loading/action.yml index 101d3c2..af387d6 100644 --- a/.github/actions/loading/action.yml +++ b/.github/actions/loading/action.yml @@ -2,7 +2,7 @@ name: Load Parameters description: | This composite action loads build parameters from a `MACHINES.json` file. It parses the JSON file to create two matrices: - - `build_matrix`: A complete matrix containing 'machine', 'firmware' and 'lavaname' + - `build_matrix`: A complete matrix containing 'machine', 'firmware', 'lavaname' and 'target' It expects the `MACHINES.json` file to be located at `ci/MACHINES.json` relative to the workspace root. outputs: @@ -37,12 +37,13 @@ runs: const matrix = Object.values(fileData) .filter(item => typeof item === 'object' && item !== null && - 'machine' in item && 'firmware' in item && 'lavaname' in item + 'machine' in item && 'firmware' in item && 'lavaname' in item && 'target' in item ) - .map(({ machine, firmware, lavaname }) => ({ + .map(({ machine, firmware, lavaname, target }) => ({ machine, firmware, - lavaname + lavaname, + target })); core.setOutput('build_matrix', JSON.stringify(matrix)); diff --git a/.github/actions/sync/action.yml b/.github/actions/sync/action.yml index 391dfc1..cb600bd 100644 --- a/.github/actions/sync/action.yml +++ b/.github/actions/sync/action.yml @@ -18,6 +18,10 @@ inputs: description: Firmware identifier type: string required: true + target: + description: target identifier + type: string + required: true outputs: workspace_path: @@ -48,7 +52,7 @@ runs: cd hexagon-dsp-binaries git fetch --all --tags --prune - TARGET="${{ inputs.firmware }}" + TARGET="${{ inputs.target }}" DSP_REF="" CONFIG="config.txt" @@ -58,8 +62,9 @@ runs: exit 1 fi while read -r _ subdir dsp version; do - echo "DSP binaries for $dsp in $subdir - $version" - if [[ "${subdir,,}" == */"${TARGET,,}" ]]; then + echo "DSP binaries for $dsp in $subdir - $version" + # match anywhere in the path + if [[ "${subdir,,}" == */"${TARGET,,}" ]]; then echo "Found matching DSP binaries for target '$TARGET': $subdir - $version" echo "Copying DSP binaries for $dsp in $subdir/$version" mkdir -p "../firmware_dir/usr/lib/dsp/${dsp}" @@ -76,9 +81,7 @@ runs: run: | cd ${{ github.workspace }} git clone https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git - TARGET_RAW="${{ inputs.firmware }}" - # Remove "-ride" suffix if present - TARGET="${TARGET_RAW%-ride}" + TARGET="${{ inputs.target }}" # Construct the source path for qcom firmware in the cloned linux-firmware repo # It's usually under /qcom/ diff --git a/.github/workflows/sync_build.yml b/.github/workflows/sync_build.yml index 58ea48e..5c09270 100644 --- a/.github/workflows/sync_build.yml +++ b/.github/workflows/sync_build.yml @@ -21,6 +21,10 @@ on: description: Firmware identifier type: string required: true + target: + description: Target identifier + type: string + required: true jobs: build: @@ -46,6 +50,7 @@ jobs: with: machine: ${{ inputs.machine }} firmware: ${{ inputs.firmware }} + target: ${{ inputs.target }} - name: Build workspace id: build_workspace diff --git a/ci/MACHINES.json b/ci/MACHINES.json index 05435ca..eb63b4a 100644 --- a/ci/MACHINES.json +++ b/ci/MACHINES.json @@ -1,12 +1,20 @@ { + "qcs6490-rb3gen2": { + "machine": "qcs6490-rb3gen2", + "firmware": "rb3gen2", + "lavaname": "qcs6490", + "target": "qcm6490" + }, "qcs9100-ride-r3": { "machine": "qcs9100-ride-r3", "firmware": "sa8775p-ride", - "lavaname": "qcs9100-ride" + "lavaname": "qcs9100-ride", + "target": "sa8775p" }, "qcs8300-ride": { "machine": "qcs8300-ride", "firmware": "qcs8300-ride", - "lavaname": "qcs8300-ride" + "lavaname": "qcs8300-ride", + "target": "qcs8300" } } \ No newline at end of file From fe39cf96efdbde438718b9025fccb7a68493229a Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Tue, 14 Oct 2025 16:35:58 +0530 Subject: [PATCH 21/40] Flexible Matching Logic for Target Path Resolution - Updated path matching logic to allow matching the target name at any position within the path, not just at the end. - Improves compatibility with diverse directory structures and enhances robustness of target-specific resolution. Signed-off-by: Tharun Kumar Merugu --- .github/actions/sync/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/sync/action.yml b/.github/actions/sync/action.yml index cb600bd..a82d16f 100644 --- a/.github/actions/sync/action.yml +++ b/.github/actions/sync/action.yml @@ -64,7 +64,7 @@ runs: while read -r _ subdir dsp version; do echo "DSP binaries for $dsp in $subdir - $version" # match anywhere in the path - if [[ "${subdir,,}" == */"${TARGET,,}" ]]; then + if [[ "${subdir,,}" == *"${TARGET,,}/"* ]]; then echo "Found matching DSP binaries for target '$TARGET': $subdir - $version" echo "Copying DSP binaries for $dsp in $subdir/$version" mkdir -p "../firmware_dir/usr/lib/dsp/${dsp}" From df3e9ac0483dbbd38d9ea0bb6e33e8c0e08c77a3 Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Tue, 14 Oct 2025 17:07:03 +0530 Subject: [PATCH 22/40] Attribute Renaming: Standardized Target Configuration Fields - Renamed all four target configuration attributes for better clarity and consistency: - `deviceTree` is now used for the device tree blob name - `linuxFirmware` now refers to the firmware identifier - `lavaDeviceName` now indicates the LAVA machine name - `hexDSPBinary` now represents the Hexagon DSP binary identifier - Updated mappings for all three targets. - Improved the readability and maintainability of build matrix definitions. Signed-off-by: Tharun Kumar Merugu --- .github/actions/aws_s3_helper/action.yml | 20 +++++------ .github/actions/lava_job_render/action.yml | 10 +++--- .github/actions/loading/action.yml | 14 ++++---- .github/actions/sync/action.yml | 42 +++++++++++----------- .github/workflows/pre_merge.yml | 2 +- .github/workflows/sync_build.yml | 21 ++++++----- .github/workflows/test.yml | 8 ++--- ci/MACHINES.json | 24 ++++++------- 8 files changed, 69 insertions(+), 72 deletions(-) diff --git a/.github/actions/aws_s3_helper/action.yml b/.github/actions/aws_s3_helper/action.yml index ad3bdda..147f5de 100644 --- a/.github/actions/aws_s3_helper/action.yml +++ b/.github/actions/aws_s3_helper/action.yml @@ -21,8 +21,8 @@ inputs: description: Mode of operation (single-upload, multi-upload, download) required: true default: single-upload - machine: - description: Target machine name or identifier, used to organize uploads in S3. + deviceTree: + description: Target device Tree name or identifier, used to organize uploads in S3. type: string required: true @@ -38,7 +38,7 @@ runs: id: sync-data shell: bash env: - UPLOAD_LOCATION: ${{ github.repository_owner }}/${{ github.event.repository.name }}/${{ github.workflow }}/${{ github.head_ref != '' && github.head_ref || github.run_id }}/${{ inputs.machine }}/ + UPLOAD_LOCATION: ${{ github.repository_owner }}/${{ github.event.repository.name }}/${{ github.workflow }}/${{ github.head_ref != '' && github.head_ref || github.run_id }}/${{ inputs.deviceTree }}/ run: | echo "::group::$(printf '__________ %-100s' 'Process' | tr ' ' _)" case "${{ inputs.mode }}" in @@ -46,8 +46,8 @@ runs: echo "Uploading files to S3 bucket..." first_line=true # Start the JSON object - mkdir -p "${{ github.workspace }}/${{ inputs.machine }}" - echo "{" > ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + mkdir -p "${{ github.workspace }}/${{ inputs.deviceTree }}" + echo "{" > ${{ github.workspace }}/${{ inputs.deviceTree }}/presigned_urls_${{ inputs.deviceTree }}.json while IFS= read -r file; do if [ -f "$file" ]; then echo "Uploading $file..." @@ -59,17 +59,17 @@ runs: if [ "$first_line" = true ]; then first_line=false else - echo "," >> ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + echo "," >> ${{ github.workspace }}/${{ inputs.deviceTree }}/presigned_urls_${{ inputs.deviceTree }}.json fi # Append the pre-signed URL to the file - echo " \"${file}\": \"${presigned_url}\"" >> ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + echo " \"${file}\": \"${presigned_url}\"" >> ${{ github.workspace }}/${{ inputs.deviceTree }}/presigned_urls_${{ inputs.deviceTree }}.json echo "Pre-signed URL for $file: $presigned_url" else echo "Warning: $file does not exist or is not a regular file." fi done < "${{ inputs.local_file }}" # Close the JSON object - echo "}" >> ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + echo "}" >> ${{ github.workspace }}/${{ inputs.deviceTree }}/presigned_urls_${{ inputs.deviceTree }}.json ;; single-upload) echo "Uploading single file to S3 bucket..." @@ -94,6 +94,6 @@ runs: if: ${{ inputs.mode == 'multi-upload' }} uses: actions/upload-artifact@v4 with: - name: presigned_urls_${{ inputs.machine }}.json - path: ${{ github.workspace }}/${{ inputs.machine }}/presigned_urls_${{ inputs.machine }}.json + name: presigned_urls_${{ inputs.deviceTree }}.json + path: ${{ github.workspace }}/${{ inputs.deviceTree }}/presigned_urls_${{ inputs.deviceTree }}.json retention-days: 3 diff --git a/.github/actions/lava_job_render/action.yml b/.github/actions/lava_job_render/action.yml index 5e51cc5..98ad026 100644 --- a/.github/actions/lava_job_render/action.yml +++ b/.github/actions/lava_job_render/action.yml @@ -33,7 +33,7 @@ runs: } const filePath = p.join( process.env.GITHUB_WORKSPACE, - `presigned_urls_${process.env.MACHINE}.json` + `presigned_urls_${process.env.DEVICE_TREE}.json` ); if (fs.existsSync(filePath)) { @@ -49,7 +49,7 @@ runs: const imageUrl = findUrlByFilename('Image'); const vmlinuxUrl = findUrlByFilename('vmlinux'); const firmwareUrl = findUrlByFilename('ramdisk_fastrpc.gz'); - const dtbFilename = `${process.env.MACHINE}.dtb`; + const dtbFilename = `${process.env.DEVICE_TREE}.dtb`; const dtbUrl = findUrlByFilename(dtbFilename); // Set outputs core.setOutput('modules_url', modulesTarUrl); @@ -76,7 +76,7 @@ runs: -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ -e dtb_url="${{ steps.process_urls.outputs.dtb_url }}" \ ${{ inputs.docker_image }} \ - jq '.artifacts["dtbs/qcom/${{ env.MACHINE }}.dtb"] = env.dtb_url' data/metadata.json > temp.json && mv temp.json data/metadata.json + jq '.artifacts["dtbs/qcom/${{ env.DEVICE_TREE }}.dtb"] = env.dtb_url' data/metadata.json > temp.json && mv temp.json data/metadata.json - name: Upload metadata.json id: upload_metadata @@ -174,8 +174,8 @@ runs: --user "$(id -u):$(id -g)" \ --workdir="$PWD" \ -v "$(dirname "$PWD")":"$(dirname "$PWD")" \ - -e TARGET="${{ env.LAVA_NAME }}" \ - -e TARGET_DTB="${{ env.MACHINE }}" \ + -e TARGET="${{ env.LAVA_DEVICE_NAME }}" \ + -e TARGET_DTB="${{ env.DEVICE_TREE }}" \ ${{ inputs.docker_image }} \ sh -c 'export BOOT_METHOD=fastboot && \ export TARGET=${TARGET} && \ diff --git a/.github/actions/loading/action.yml b/.github/actions/loading/action.yml index af387d6..5b82e0c 100644 --- a/.github/actions/loading/action.yml +++ b/.github/actions/loading/action.yml @@ -2,7 +2,7 @@ name: Load Parameters description: | This composite action loads build parameters from a `MACHINES.json` file. It parses the JSON file to create two matrices: - - `build_matrix`: A complete matrix containing 'machine', 'firmware', 'lavaname' and 'target' + - `build_matrix`: A complete matrix containing 'deviceTree', 'linuxFirmware', 'lavaDeviceName' and 'hexDSPBinary' It expects the `MACHINES.json` file to be located at `ci/MACHINES.json` relative to the workspace root. outputs: @@ -37,13 +37,13 @@ runs: const matrix = Object.values(fileData) .filter(item => typeof item === 'object' && item !== null && - 'machine' in item && 'firmware' in item && 'lavaname' in item && 'target' in item + 'deviceTree' in item && 'linuxFirmware' in item && 'lavaDeviceName' in item && 'hexDSPBinary' in item ) - .map(({ machine, firmware, lavaname, target }) => ({ - machine, - firmware, - lavaname, - target + .map(({ deviceTree, linuxFirmware, lavaDeviceName, hexDSPBinary }) => ({ + deviceTree, + linuxFirmware, + lavaDeviceName, + hexDSPBinary })); core.setOutput('build_matrix', JSON.stringify(matrix)); diff --git a/.github/actions/sync/action.yml b/.github/actions/sync/action.yml index a82d16f..6eb3ce1 100644 --- a/.github/actions/sync/action.yml +++ b/.github/actions/sync/action.yml @@ -1,25 +1,22 @@ name: Sync workspace description: | - This composite action synchronizes a workspace by cleaning it up, - checking out necessary code repositories, and preparing a firmware - directory with required binaries. It clones and copies FastRPC hexagon - DSP binaries and Linux firmware (specifically QCOM firmware) - based on the provided `firmware` input. It also clones the - `qualcomm-linux/kernel` repository. - The action sets an output `workspace_path` to the GitHub workspace - directory. + This composite action synchronizes a workspace by cleaning it + up,checking out necessary code repositories, and preparing a + linux Firmware directory with required binaries. + It clones and copies FastRPC hexagon DSP binaries and + Linux firmware (specifically QCOM firmware) based on + the provided `hexDSPBinary` and `linuxFirmware` input. + It also clones the `qualcomm-linux/kernel` repository. + The action sets an output `workspace_path` to the GitHub + workspace directory. inputs: - machine: - description: Target machine name + linuxFirmware: + description: Linux Firmware identifier type: string required: true - firmware: - description: Firmware identifier - type: string - required: true - target: - description: target identifier + hexDSPBinary: + description: Hexagon DSP binaries identifier type: string required: true @@ -52,7 +49,7 @@ runs: cd hexagon-dsp-binaries git fetch --all --tags --prune - TARGET="${{ inputs.target }}" + TARGET="${{ inputs.hexDSPBinary }}" DSP_REF="" CONFIG="config.txt" @@ -81,7 +78,8 @@ runs: run: | cd ${{ github.workspace }} git clone https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git - TARGET="${{ inputs.target }}" + TARGET="${{ inputs.hexDSPBinary }}" + LINUX_FIRMWARE="${{ inputs.linuxFirmware }}" # Construct the source path for qcom firmware in the cloned linux-firmware repo # It's usually under /qcom/ @@ -89,16 +87,16 @@ runs: # Check if the target qcom firmware directory exists in the cloned repo if [ ! -d "$LINUX_FIRMWARE_QCOM_PATH" ]; then - echo "ERROR: Directory '$LINUX_FIRMWARE_QCOM_PATH' not found in linux-firmware repo." + echo "ERROR: Directory '$LINUX_FIRMWARE_QCOM_PATH' not found in linux_firmware repo." exit -1 else # Create the destination directory in firmware_dir - mkdir -p "${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${TARGET}" + mkdir -p "${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${LINUX_FIRMWARE}" # Copy the contents of the qcom target directory # Use trailing slashes to copy contents, not the directory itself - cp -rf "${LINUX_FIRMWARE_QCOM_PATH}/"* "${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${TARGET}/" - echo "Copied QCOM firmware from '${LINUX_FIRMWARE_QCOM_PATH}' to '${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${TARGET}/' successfully!" + cp -rf "${LINUX_FIRMWARE_QCOM_PATH}/"* "${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${LINUX_FIRMWARE}/" + echo "Copied QCOM firmware from '${LINUX_FIRMWARE_QCOM_PATH}' to '${{ github.workspace }}/firmware_dir/usr/lib/firmware/qcom/${LINUX_FIRMWARE}/' successfully!" fi - name: List all subdirectories and files in firmware_dir diff --git a/.github/workflows/pre_merge.yml b/.github/workflows/pre_merge.yml index 68b9411..41a5fa7 100644 --- a/.github/workflows/pre_merge.yml +++ b/.github/workflows/pre_merge.yml @@ -41,7 +41,7 @@ jobs: docker_image: fastrpc-image:latest deviceTree: ${{ matrix.deviceTree }} linuxFirmware: ${{ matrix.linuxFirmware }} - dspHexBinary: ${{ matrix.dspHexBinary }} + hexDSPBinary: ${{ matrix.hexDSPBinary }} # The test job will run on the machines specified in the build_matrix output # from the loading job. It will use the test.yml workflow to run tests. diff --git a/.github/workflows/sync_build.yml b/.github/workflows/sync_build.yml index 5c09270..f886c00 100644 --- a/.github/workflows/sync_build.yml +++ b/.github/workflows/sync_build.yml @@ -13,16 +13,16 @@ on: description: Docker image type: string required: true - machine: - description: Target machine name + deviceTree: + description: device Tree name type: string required: true - firmware: - description: Firmware identifier + linuxFirmware: + description: Linux Firmware identifier type: string required: true - target: - description: Target identifier + hexDSPBinary: + description: Hexagon DSP binaries identifier type: string required: true @@ -48,9 +48,8 @@ jobs: id: sync uses: qualcomm/fastrpc/.github/actions/sync@development with: - machine: ${{ inputs.machine }} - firmware: ${{ inputs.firmware }} - target: ${{ inputs.target }} + linuxFirmware: ${{ inputs.linuxFirmware }} + hexDSPBinary: ${{ inputs.hexDSPBinary }} - name: Build workspace id: build_workspace @@ -73,7 +72,7 @@ jobs: $workspace/kobj/arch/arm64/boot/Image $workspace/kobj/vmlinux $workspace/artifacts/ramdisk_fastrpc.gz - $workspace/kobj/arch/arm64/boot/dts/qcom/${{ inputs.machine }}.dtb + $workspace/kobj/arch/arm64/boot/dts/qcom/${{ inputs.deviceTree }}.dtb EOF echo "Generated artifact list:" @@ -86,7 +85,7 @@ jobs: # local_file points to the list of files to upload local_file: ${{ steps.sync.outputs.workspace_path }}/artifacts/file_list.txt mode: multi-upload - machine: ${{ inputs.machine }} + deviceTree: ${{ inputs.deviceTree }} - name: Clean up # This step is crucial for self-hosted runners to manage disk space. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d229ed6..c9de0aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,7 @@ jobs: - name: Download URLs list uses: actions/download-artifact@v4 with: - name: presigned_urls_${{ matrix.build_matrix.machine }}.json + name: presigned_urls_${{ matrix.build_matrix.deviceTree }}.json merge-multiple: true path: ${{ github.workspace }} @@ -59,9 +59,9 @@ jobs: with: docker_image: ${{ inputs.docker_image }} env: - FIRMWARE: ${{ matrix.build_matrix.firmware }} - MACHINE: ${{ matrix.build_matrix.machine }} - LAVA_NAME: ${{ matrix.build_matrix.lavaname }} + FIRMWARE: ${{ matrix.build_matrix.linuxFirmware }} + DEVICE_TREE: ${{ matrix.build_matrix.deviceTree }} + LAVA_DEVICE_NAME: ${{ matrix.build_matrix.lavaDeviceName }} - name: Submit lava job id: submit_job diff --git a/ci/MACHINES.json b/ci/MACHINES.json index eb63b4a..44d8ecd 100644 --- a/ci/MACHINES.json +++ b/ci/MACHINES.json @@ -1,20 +1,20 @@ { "qcs6490-rb3gen2": { - "machine": "qcs6490-rb3gen2", - "firmware": "rb3gen2", - "lavaname": "qcs6490", - "target": "qcm6490" + "deviceTree": "qcs6490-rb3gen2", + "linuxFirmware": "qcs6490", + "lavaDeviceName": "qcs6490", + "hexDSPBinary": "qcm6490" }, "qcs9100-ride-r3": { - "machine": "qcs9100-ride-r3", - "firmware": "sa8775p-ride", - "lavaname": "qcs9100-ride", - "target": "sa8775p" + "deviceTree": "qcs9100-ride-r3", + "linuxFirmware": "sa8775p", + "lavaDeviceName": "qcs9100-ride", + "hexDSPBinary": "sa8775p" }, "qcs8300-ride": { - "machine": "qcs8300-ride", - "firmware": "qcs8300-ride", - "lavaname": "qcs8300-ride", - "target": "qcs8300" + "deviceTree": "qcs8300-ride", + "linuxFirmware": "qcs8300", + "lavaDeviceName": "qcs8300-ride", + "hexDSPBinary": "qcs8300" } } \ No newline at end of file From 1316edffdbbfbae2dcf36df5230c67b9e36c4e8c Mon Sep 17 00:00:00 2001 From: Thirumalai Nagalingam <81082960+thiru31@users.noreply.github.com> Date: Sun, 19 Oct 2025 15:57:04 +0000 Subject: [PATCH 23/40] dsprpcd: dlopen versioned listener libraries with fallback Currently dsprpcd attempts to dlopen() unversioned libraries such as libadsp_default_listener.so, This breaks on distributions like Debian, which only ship ABI-versioned libraries in runtime packages. It also risks binding to a different ABI in the future if a .so.2 is introduced as mentioned in issue-230. This patch updates the default listener library names to include .so.1, and adds a fallback to the unversioned .so for environments that still ship only the unversioned file. Signed-off-by: Thirumalai Nagalingam <81082960+thiru31@users.noreply.github.com> --- src/dsprpcd.c | 78 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/src/dsprpcd.c b/src/dsprpcd.c index b3d01f8..348049a 100644 --- a/src/dsprpcd.c +++ b/src/dsprpcd.c @@ -15,39 +15,80 @@ #include #include -#ifndef ADSP_DEFAULT_LISTENER_NAME -#define ADSP_DEFAULT_LISTENER_NAME "libadsp_default_listener.so" +#ifndef ADSP_LISTENER_VERSIONED +#define ADSP_LISTENER_VERSIONED "libadsp_default_listener.so.1" +#define ADSP_LISTENER_UNVERSIONED "libadsp_default_listener.so" #endif -#ifndef CDSP_DEFAULT_LISTENER_NAME -#define CDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so" +#ifndef CDSP_LISTENER_VERSIONED +#define CDSP_LISTENER_VERSIONED "libcdsp_default_listener.so.1" +#define CDSP_LISTENER_UNVERSIONED "libcdsp_default_listener.so" #endif -#ifndef SDSP_DEFAULT_LISTENER_NAME -#define SDSP_DEFAULT_LISTENER_NAME "libsdsp_default_listener.so" +#ifndef SDSP_LISTENER_VERSIONED +#define SDSP_LISTENER_VERSIONED "libsdsp_default_listener.so.1" +#define SDSP_LISTENER_UNVERSIONED "libsdsp_default_listener.so" #endif -#ifndef GDSP_DEFAULT_LISTENER_NAME -#define GDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so.1" +#ifndef GDSP_LISTENER_VERSIONED +#define GDSP_LISTENER_VERSIONED "libcdsp_default_listener.so.1" +#define GDSP_LISTENER_UNVERSIONED "libcdsp_default_listener.so" #endif typedef int (*dsp_default_listener_start_t)(int argc, char *argv[]); +// Result struct for dlopen. +struct dlopen_result { + void *handle; + const char *loaded_lib_name; +}; + +/** + * Attempts to load a shared library using dlopen. + * If the versioned name fails, falls back to the unversioned name. + * Returns both the handle and the name of the library successfully loaded. + */ +static struct dlopen_result try_dlopen(const char *versioned, const char *unversioned) { + struct dlopen_result result = { NULL, NULL }; + + result.handle = dlopen(versioned, RTLD_NOW); + if (result.handle) { + result.loaded_lib_name = versioned; + return result; + } + + if (unversioned) { + VERIFY_IPRINTF("dlopen failed for %s: %s; attempting fallback %s", + versioned, dlerror(), unversioned); + result.handle = dlopen(unversioned, RTLD_NOW); + if (result.handle) { + result.loaded_lib_name = unversioned; + return result; + } + } + return result; +} + int main(int argc, char *argv[]) { int nErr = 0; - void *dsphandler = NULL; - const char* lib_name; + struct dlopen_result dlres = { NULL, NULL }; + const char* lib_versioned; + const char* lib_unversioned; const char* dsp_name; dsp_default_listener_start_t listener_start; #ifdef USE_ADSP - lib_name = ADSP_DEFAULT_LISTENER_NAME; + lib_versioned = ADSP_LISTENER_VERSIONED; + lib_unversioned = ADSP_LISTENER_UNVERSIONED; dsp_name = "ADSP"; #elif defined(USE_SDSP) - lib_name = SDSP_DEFAULT_LISTENER_NAME; + lib_versioned = SDSP_LISTENER_VERSIONED; + lib_unversioned = SDSP_LISTENER_UNVERSIONED; dsp_name = "SDSP"; #elif defined(USE_CDSP) - lib_name = CDSP_DEFAULT_LISTENER_NAME; + lib_versioned = CDSP_LISTENER_VERSIONED; + lib_unversioned = CDSP_LISTENER_UNVERSIONED; dsp_name = "CDSP"; #elif defined(USE_GDSP) - lib_name = GDSP_DEFAULT_LISTENER_NAME; + lib_versioned = GDSP_LISTENER_VERSIONED; + lib_unversioned = GDSP_LISTENER_UNVERSIONED; dsp_name = "GDSP"; #else goto bail; @@ -55,14 +96,15 @@ int main(int argc, char *argv[]) { VERIFY_EPRINTF("%s daemon starting", dsp_name); while (1) { - if (NULL != (dsphandler = dlopen(lib_name,RTLD_NOW))) { + dlres = try_dlopen(lib_versioned, lib_unversioned); + if (NULL != dlres.handle) { if (NULL != (listener_start = (dsp_default_listener_start_t)dlsym( - dsphandler, "adsp_default_listener_start"))) { + dlres.handle, "adsp_default_listener_start"))) { VERIFY_IPRINTF("adsp_default_listener_start called"); nErr = listener_start(argc, argv); } - if (0 != dlclose(dsphandler)) { - VERIFY_EPRINTF("dlclose failed for %s", lib_name); + if (0 != dlclose(dlres.handle)) { + VERIFY_EPRINTF("dlclose failed for %s", dlres.loaded_lib_name); } } else { VERIFY_EPRINTF("%s daemon error %s", dsp_name, dlerror()); From 12e90f2ba4aa608970be69e3cd51db491c707cc2 Mon Sep 17 00:00:00 2001 From: Srinivas Kandagatla Date: Mon, 3 Nov 2025 07:28:01 +0000 Subject: [PATCH 24/40] inc: add required include files at instal Install AEEStdDef.h, HAP_debug.h and HAP_farf.h that are required by to compile the skel and stub files generated by qaic compiler. qaic compiler includes these header files, however fastrpc does not install these files, fix this by installing the required files. Signed-off-by: Srinivas Kandagatla --- inc/Makefile.am | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inc/Makefile.am b/inc/Makefile.am index 24f281f..fa89ed3 100644 --- a/inc/Makefile.am +++ b/inc/Makefile.am @@ -1,16 +1,16 @@ # Export fastrpc headers fastrpc_includedir = $(includedir)/fastrpc fastrpc_include_HEADERS = $(top_srcdir)/inc/AEEStdErr.h +fastrpc_include_HEADERS += $(top_srcdir)/inc/AEEStdDef.h fastrpc_include_HEADERS += $(top_srcdir)/inc/remote.h fastrpc_include_HEADERS += $(top_srcdir)/inc/rpcmem.h +fastrpc_include_HEADERS += $(top_srcdir)/inc/HAP_farf.h +fastrpc_include_HEADERS += $(top_srcdir)/inc/HAP_debug.h noinst_HEADERS = \ AEEBufBound.h \ AEEQList.h \ - AEEStdDef.h \ AEEstd.h \ - HAP_debug.h \ - HAP_farf.h \ HAP_farf_internal.h \ HAP_pls.h \ adsp_current_process.h \ From f0947c8efcc60e0e276155fef396c4278725574d Mon Sep 17 00:00:00 2001 From: Aman Pandey Date: Tue, 4 Nov 2025 10:37:33 +0530 Subject: [PATCH 25/40] Move pthread_key_create initialization earlier in fastrpc_apps_user_init Ensures tlsKey is set up before any components that might rely on it. Prevents redundant or conflicting initialization by removing duplicate pthread_key_create call. Aligns initialization order for improved stability and correctness. Signed-off-by: Aman Pandey --- src/fastrpc_apps_user.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index 81fef0a..f1ee7cd 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -4171,6 +4171,7 @@ static int fastrpc_apps_user_init(void) { VERIFY(AEE_SUCCESS == (nErr = PL_INIT(gpls))); VERIFY(AEE_SUCCESS == (nErr = PL_INIT(rpcmem))); + VERIFY(AEE_SUCCESS == (nErr = pthread_key_create(&tlsKey, exit_thread))); fastrpc_mem_init(); fastrpc_context_table_init(); fastrpc_log_init(); @@ -4201,7 +4202,6 @@ static int fastrpc_apps_user_init(void) { pthread_mutex_init(&hlist[i].async_init_deinit_mut, 0); } listener_android_init(); - VERIFY(AEE_SUCCESS == (nErr = pthread_key_create(&tlsKey, exit_thread))); VERIFY(AEE_SUCCESS == (nErr = PL_INIT(apps_std))); GenCrc32Tab(POLY32, crc_table); fastrpc_notif_init(); From bffbd35bc3317aaf61ac7c6e7726ffa6eeaf0992 Mon Sep 17 00:00:00 2001 From: Jianping Li Date: Mon, 3 Nov 2025 18:39:42 +0800 Subject: [PATCH 26/40] Fix PD exception logging When PD exception logging is enabled by default, RootPD daemons will be blocked as adspmsgd_init will wait for listener which is not initialized by the time it is called. Initialize listener before adspmsgd initialization. Signed-off-by: Jianping Li --- src/fastrpc_apps_user.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index 6de574a..4a3ffe3 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -4040,13 +4040,6 @@ static int domain_init(int domain, int *dev) { ret == (int)(DSP_AEE_EOFFSET + AEE_EUNSUPPORTED), ret); } - if ((dom != SDSP_DOMAIN_ID) && hlist[domain].dsppd == ROOT_PD) { - remote_handle64 handle = 0; - handle = get_adspmsgd_adsp1_handle(domain); - if (handle != INVALID_HANDLE) { - adspmsgd_init(handle, 0x10); // enable PD exception logging - } - } fastrpc_perf_init(hlist[domain].dev, domain); VERIFY(AEE_SUCCESS == (nErr = fastrpc_latency_init(hlist[domain].dev, &hlist[domain].qos))); @@ -4057,6 +4050,13 @@ static int domain_init(int domain, int *dev) { VERIFY(AEE_SUCCESS == (nErr = listener_android_domain_init( domain, hlist[domain].th_params.update_requested, &hlist[domain].th_params.r_sem))); + if ((dom != SDSP_DOMAIN_ID) && hlist[domain].dsppd == ROOT_PD) { + remote_handle64 handle = 0; + handle = get_adspmsgd_adsp1_handle(domain); + if (handle != INVALID_HANDLE) { + adspmsgd_init(handle, 0x10); // enable PD exception logging + } + } bail: if (nErr != AEE_SUCCESS) { domain_deinit(domain); From 677e61e699743fff69c345b7c855b6e1e5883f4f Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Fri, 21 Nov 2025 12:34:59 +0530 Subject: [PATCH 27/40] Enable ARMOR public API compatibility checks and header validation - Added config.yaml with Project Name, Branch Name, Mode: blocking, and Public Header Paths - Integrated ARMOR into PR event-based workflow to validate public headers - Introduced API breakage checks on every PR to ensure backward compatibility Signed-off-by: Tharun Kumar Merugu --- public_headers/config.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 public_headers/config.yaml diff --git a/public_headers/config.yaml b/public_headers/config.yaml new file mode 100644 index 0000000..6c48ecf --- /dev/null +++ b/public_headers/config.yaml @@ -0,0 +1,11 @@ +project: https://github.com/qualcomm/fastrpc + +branches: + development: + modes: + blocking: + headers: + - inc/remote.h + - inc/remote64.h + - inc/dspqueue.h + - inc/rpcmem.h \ No newline at end of file From ff539be3b6b0415bb12546b06c6047c4cc43fb11 Mon Sep 17 00:00:00 2001 From: Tharun Kumar Merugu Date: Wed, 26 Nov 2025 17:42:09 +0530 Subject: [PATCH 28/40] Add GitHub Actions workflow for ARMOR API/ABI monitoring - Adds preflight checks to detect API/ABI breakages early - Uses Clang AST for syntactic/semantic analysis on public headers - Improves backward compatibility with custom CSE-aligned rules Signed-off-by: Tharun Kumar Merugu --- .github/workflows/run_armor.yml | 72 ++++++++++++++++++++++ public_headers/{config.yaml => config.yml} | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/run_armor.yml rename public_headers/{config.yaml => config.yml} (88%) diff --git a/.github/workflows/run_armor.yml b/.github/workflows/run_armor.yml new file mode 100644 index 0000000..fa28806 --- /dev/null +++ b/.github/workflows/run_armor.yml @@ -0,0 +1,72 @@ +# This GitHub Actions workflow runs the ARMOR security scanning tool under the following conditions: +# - On push events to 'main' and 'development' branches +# - On pull_request_target events for 'main' and 'development' branches (to handle forked PRs) +# - Manually via workflow_dispatch with required inputs (branch-name, head-sha, base-sha) +# +# The workflow: +# 1. Sets appropriate permissions for repository access and status reporting +# 2. Checks out the repository code +# 3. Dynamically determines event context (head/base SHAs, branch name) across all trigger types +# 4. Executes the ARMOR tool with the collected parameters to perform API compatibility checks and header validation + +name: Run ARMOR via action +on: + push: + branches: [ main, development ] + pull_request_target: + branches: [ main, development ] + workflow_dispatch: + inputs: + branch-name: + description: 'Branch name to scan' + required: true + type: string + head-sha: + description: 'Head commit SHA' + required: true + type: string + base-sha: + description: 'The commit SHA that serves for comparison or analysis' + required: true + type: string + +permissions: + contents: read + pull-requests: write + statuses: write + +jobs: + RUN-ARMOR: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Set event variables + id: ev + run: | + echo "event_name=${{ github.event_name }}" >> "$GITHUB_OUTPUT" + if [[ "${{ github.event_name }}" == "pull_request_target" ]]; then + echo "head_sha=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT" + echo "base_sha=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT" + echo "branch_name=${{ github.event.pull_request.base.ref }}" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "head_sha=${{ inputs.head-sha }}" >> "$GITHUB_OUTPUT" + echo "base_sha=${{ inputs.base-sha }}" >> "$GITHUB_OUTPUT" + echo "branch_name=${{ inputs.branch-name }}" >> "$GITHUB_OUTPUT" + else + # push + echo "head_sha=${{ github.event.after }}" >> "$GITHUB_OUTPUT" + echo "base_sha=${{ github.event.before }}" >> "$GITHUB_OUTPUT" + echo "branch_name=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + fi + echo "ref=${{ github.ref }}" >> "$GITHUB_OUTPUT" + echo "repo=${{ github.repository }}" >> "$GITHUB_OUTPUT" + + - name: Run ARMOR Tool + uses: qualcomm/armor@main + with: + event-name: ${{ steps.ev.outputs.event_name }} + head-sha: ${{ steps.ev.outputs.head_sha }} + base-sha: ${{ steps.ev.outputs.base_sha }} + ref: ${{ steps.ev.outputs.ref }} + repo: ${{ steps.ev.outputs.repo }} + branch-name: ${{ steps.ev.outputs.branch_name }} \ No newline at end of file diff --git a/public_headers/config.yaml b/public_headers/config.yml similarity index 88% rename from public_headers/config.yaml rename to public_headers/config.yml index 6c48ecf..c4bd378 100644 --- a/public_headers/config.yaml +++ b/public_headers/config.yml @@ -8,4 +8,4 @@ branches: - inc/remote.h - inc/remote64.h - inc/dspqueue.h - - inc/rpcmem.h \ No newline at end of file + - inc/rpcmem.h From cdb828b7595710c8a5192485dbc15e59e8e7ab3a Mon Sep 17 00:00:00 2001 From: Vinayak Katoch Date: Tue, 9 Dec 2025 16:34:07 +0530 Subject: [PATCH 29/40] test: Update examples and test runner with improved error handling - Update hap_example with proper handling for skip and failed cases - Add multithreading example computing parallel sum - Update fastrpc_test runner to handle pass, failed, and skipped cases - Improve test result reporting and categorization Signed-off-by: Vinayak Katoch --- test/fastrpc_test.c | 49 +++++++++++++++++++++++------ test/linux/libhap_example.so | Bin 54920 -> 56672 bytes test/linux/libmultithreading.so | Bin 38864 -> 38616 bytes test/v68/libhap_example_skel.so | Bin 66452 -> 62320 bytes test/v68/libmultithreading_skel.so | Bin 33420 -> 33420 bytes test/v75/libhap_example_skel.so | Bin 66452 -> 62320 bytes test/v75/libmultithreading_skel.so | Bin 33420 -> 33420 bytes 7 files changed, 40 insertions(+), 9 deletions(-) diff --git a/test/fastrpc_test.c b/test/fastrpc_test.c index 89f57e2..1ce506b 100644 --- a/test/fastrpc_test.c +++ b/test/fastrpc_test.c @@ -11,6 +11,8 @@ #include #include // For PATH_MAX +#define DSP_AEE_EUNSUPPORTED 0x80000414 + typedef int (*run_test_t)(int domain_id, bool is_unsignedpd_enabled); static void print_usage() { @@ -49,6 +51,10 @@ int main(int argc, char *argv[]) { void *lib_handle = NULL; run_test_t run_test = NULL; int nErr = 0; + int tests_run = 0; + int tests_passed = 0; + int tests_failed = 0; + int tests_skipped = 0; int opt; while ((opt = getopt(argc, argv, "d:U:t:a:")) != -1) { @@ -127,10 +133,17 @@ int main(int argc, char *argv[]) { } nErr = run_test(domain_id, is_unsignedpd_enabled); - if (nErr != 0) { - printf("Test failed with error code 0x%x in %s\n", nErr, full_lib_path); + tests_run++; + + if (nErr == 0) { + tests_passed++; + printf("[PASS] %s\n\n", entry->d_name); + } else if (nErr == DSP_AEE_EUNSUPPORTED) { + tests_skipped++; + printf("[SKIP] %s (not applicable to this hardware)\n\n", entry->d_name); } else { - printf("Success in %s\n", full_lib_path); + tests_failed++; + printf("[FAIL] %s (error code: 0x%x)\n\n", entry->d_name, nErr); } dlclose(lib_handle); @@ -139,11 +152,29 @@ int main(int argc, char *argv[]) { closedir(dir); - if (nErr != 0) { - printf("Test failed with error code 0x%x\n", nErr); - } else { - printf("All tests completed successfully\n"); + printf("\n========================================\n"); + printf("Test Summary:\n"); + printf(" Total tests run: %d\n", tests_run); + printf(" Passed: %d\n", tests_passed); + printf(" Failed: %d\n", tests_failed); + printf(" Skipped: %d\n", tests_skipped); + printf("========================================\n"); + + if (tests_run == 0) { + printf("\nERROR: No tests were found or executed.\n"); + return -1; + } + + if (tests_failed > 0) { + printf("\nRESULT: %d test(s) FAILED\n", tests_failed); + return tests_failed; } - return nErr; -} \ No newline at end of file + if (tests_passed == 0 && tests_skipped > 0) { + printf("\nWARNING: All tests were skipped. No applicable tests for this hardware.\n"); + return -1; + } + + printf("\nRESULT: All applicable tests PASSED\n"); + return 0; +} diff --git a/test/linux/libhap_example.so b/test/linux/libhap_example.so index b10cda6bf7d81fafc5aa5346c4c019d7999fafa6..05f2b2a4c8aa63b2b4b3bb53944c4932137c39c1 100644 GIT binary patch literal 56672 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%0Ao0l`*=gyswk?I_t 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 delta 909 zcmV;819JR~g#wI)0AiH-y(j_OYe!`ZKQaAm$;XrM>16}0k=ZS=?-P8V?r<14< zAb()6*`Hf)$yW+-8E%7>OzswcX2!_ZO7mYKVKOr)Vv3)|Z)F5iSB{IT`i=t_e(=Jj zQy^>Aq7z%m^1d zFoFbiFoFYJpaTK{0s<6nE+zykVq(D~sg3yn00EajG5ctf>k)w&Fg7qXFgP+aGC5io z4Kp<`HZU_VI5IUdIkSZlO#y$t3L8I9;A670TcZ}1M#o@-d2Qt{(l;x#nU?lh@^}xc ziKUie6^etMWRP19x{3b@@rChPR6QwLV994BHBBcM%N6Vz1(~UFKEHUFr?Vx%Ye^P5 zn4;vl&3e<`gKc*8G;^0Abq#u>`I(*v8m(D>!=@%-a8yXGR?+Oy#z z`2)~=Jx?gsg6{_l9ak*CcH7lePZPFOJw7RJSJ&6dxwPyE#<-vJ;`n# ziW@OeSI@OC_Kx1&IlKaHSCi5dZ-1wKT;4)F&^m1glK7XYvLr~m9j|R_#LSKy{^)6W=WCA}%s!=!eHDk<)m9T;TUGRsws1zd3l}I}Vz1I}( jn1xvq=aHC(+{&lH>{s_jaRN5avq2UIYXLyBNr6KFh^?iz diff --git a/test/v75/libhap_example_skel.so b/test/v75/libhap_example_skel.so index 0fc4888646221ffffed259fefc6bf08e363ed87e..0dc42a2d0a2e42d1a4e72c146921d439881009fd 100644 GIT binary patch literal 62320 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 KeDnWH*Z%|h2b{40 literal 66452 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 delta 909 zcmV;819JR~g#wI)0AiH-y(j_OYe!`ZKQaAm$;XrM>16}0k=ZS=?-P8V?r<14< zAb(MjW5o#x4%NZE>NCGiwR>S(wJ8H1EPa;}>g;y_sw$y}I^ePpc$PJ9?l0ApP_=CP z^v>YF-dpQR&GSkZ@$O*dNB!slS8J3dHdd=`YuTcJ2k3Wq@FdK3nfkmlISBnh8wUZ+ zA2VljI7F=IgMB*_Gdv%Z8jui77J?b)$A7(i;k)w&Fg7qXFgP+YGcj5h z4Kp<`HZU_VI5IOcF|&mdO#y$#iRe}?DN{m292Y!#rM)wo8NfK{xR;Vg%n@hl#|ahb zqv6@yv2BzjuHn!^I3-|4W}z7;6=q)xq*lZ?ZJc$Uz84fJ?e+Iz-g=MX2rbwoianws z4v!7x*dbt%u$^Rs`u9Ohes+MYa;!9x`=pb7X4H^1d}O{PYfF!n0lI(oFO2y6ro_B_ ztc(ft9yArB;`CRuYKs6>9Lk_^Iv8qXeWu_ zVmX6IjbkT??D?1aMVfHPt{$VXuI(7uMRf0R(%<#kux{n;Zve0?(?)+&)Pai8`P8iV zpREoQEYaIY=QY~5$+rSRvy;*kZ-1bjs5sav!Bn{pBJ8Re3GS~RC|l)INX6(i0D6(6 znousxw-wWq95iylJ%cP~?iyMQb)PR;D)Z@;PP(Y>K`myu0DN6dnBk9_ zjRyg_PYn=rSkrBezEq#%J4IGADuHu$L0^judcc)dDw?k;VjoY#CZ8GW#f5v;E!VM jZ6~1DO-G6vwUo6P-00~d#sUO)vq2UIYXLyBNr6KF*W$D5 From ab185878e2af961b901e66db9aa15da90415e296 Mon Sep 17 00:00:00 2001 From: Abhinav Parihar Date: Thu, 4 Sep 2025 17:23:28 +0530 Subject: [PATCH 30/40] Support dynamic DSP path resolution via conf directory Update DSP mount and search path configuration to support dynamic runtime resolution. Paths are now resolved based on platform-specific entries defined in the conf directory, allowing flexible DSP library location management Co-authored-by: Vinayak Katoch Signed-off-by: Abhinav Parihar --- Docs/conf_guideline.md | 94 ++++++++++++ Docs/schemas/fastrpc-config-schema.yaml | 1 + configure.ac | 20 +++ inc/Makefile.am | 1 + inc/apps_std_internal.h | 25 +--- inc/fastrpc_config_parser.h | 17 +++ inc/fastrpc_internal.h | 2 +- src/Makefile.am | 20 ++- src/apps_std_imp.c | 97 +++++++------ src/fastrpc_apps_user.c | 50 +++---- src/fastrpc_config_parser.c | 181 ++++++++++++++++++++++++ src/log_config.c | 5 +- 12 files changed, 416 insertions(+), 97 deletions(-) create mode 100644 Docs/conf_guideline.md create mode 100644 Docs/schemas/fastrpc-config-schema.yaml create mode 100644 inc/fastrpc_config_parser.h create mode 100644 src/fastrpc_config_parser.c diff --git a/Docs/conf_guideline.md b/Docs/conf_guideline.md new file mode 100644 index 0000000..ff303a6 --- /dev/null +++ b/Docs/conf_guideline.md @@ -0,0 +1,94 @@ +📄 **YAML Configuration Usage Guide** + +--- + +### 🔧 **Purpose** +The YAML configuration file enables **fastrpc** to set machine-specific configurations at runtime. Each machine entry corresponds to a specific hardware platform. + +- fastrpc supports reading YAML configuration files from a particular directory. Users should ensure all configuration files are stored in that same directory. + - For Linux platforms: `/usr/share/qcom/conf.d/` +- In case of multiple configuration files defining path for a single machine, the directory is parsed in lexicographical order and the latest one carrying the + machine path is picked. +- **Machine Name**: Obtain the machine name for your platform from: + ``` + /sys/firmware/devicetree/base/model + ``` + (fastrpc uses same path for matching machine names) +--- +### 📄 **Current Properties** +- **DSP_LIBRARY_PATH**: Specifies the path to DSP binaries and resources for the Machine. +--- + +### 📁 **Format Guidelines** +The configuration uses YAML format with the following structure: +``` +machines: + "Machine Name": + DSP_LIBRARY_PATH: "/relative/path/to/dsp/binaries/" +``` + +**Key Points:** +- The root element is `machines:` +- Each machine name is a quoted string key under `machines:` +- Properties are indented under each machine name +- Use proper YAML indentation +- Paths should be quoted strings + +--- + +### ✅ **Example Configuration** +``` +machines: + "Qualcomm Technologies, Inc. DB820c": + DSP_LIBRARY_PATH: "/apq8096/Qualcomm/db820c/" + "Thundercomm Dragonboard 845c": + DSP_LIBRARY_PATH: "/sdm845/Thundercomm/db845c/" +``` + +--- + +### ⚠️ **Important Notes** +- Do **not** modify machine names unless adding a new supported Machine. +- Ensure `DSP_LIBRARY_PATH` values: + - Are enclosed in double quotes (`"..."`). + - Are **relative to `/usr/share/qcom/`**. +- Follow YAML syntax rules: + - Use consistent indentation. + - Ensure proper spacing after colons (`: `). + - Quote strings containing special characters or spaces. + - Avoid tabs (use spaces only). +- Maintain: + - Proper YAML structure and hierarchy. + - Consistent formatting across entries. +- When adding new properties: + - Document their purpose **here**. + - Follow the same indentation pattern. +- Do **not** create duplicate Machine entries. +- Validate YAML syntax before deployment to avoid parsing errors. + +--- + +### ➕ **Adding New Platforms** +To add a new Machine, follow the existing YAML format: +``` +machines: + "New Machine Name": + DSP_LIBRARY_PATH: "/new_machine/path/" +``` + +Ensure the new entry is properly indented under the `machines:` root element and follows YAML syntax conventions. + +--- + +### 📝 **File Naming** +Configuration files should use the `.yaml` or `.yml` extension and be placed in the designated configuration directory (`/usr/share/qcom/conf.d/` on Linux platforms). + +### ✅ Schema Validation +To ensure the configuration file adheres to the required structure, validate it against the schema provided. + +Schema File Location: +/Docs/schemas/fastrpc-config-schema.yaml + +Validation Command: +Use Yamale for schema validation: +yamale -s fastrpc-config-schema.yaml \ No newline at end of file diff --git a/Docs/schemas/fastrpc-config-schema.yaml b/Docs/schemas/fastrpc-config-schema.yaml new file mode 100644 index 0000000..105efdc --- /dev/null +++ b/Docs/schemas/fastrpc-config-schema.yaml @@ -0,0 +1 @@ +machines: map(key=str(), value=map(DSP_LIBRARY_PATH=regex('^/.+/'))) \ No newline at end of file diff --git a/configure.ac b/configure.ac index 4c6d763..ca83f2d 100644 --- a/configure.ac +++ b/configure.ac @@ -43,6 +43,26 @@ AM_PROG_CC_C_O # Checks for library functions. +# Enable pkg-config +PKG_PROG_PKG_CONFIG + +# Check for libyaml only if not Android +AS_IF([test "$compile_for_android" = no], [ + PKG_CHECK_MODULES([YAML], [yaml-0.1], [], + [AC_MSG_ERROR([libyaml (yaml-0.1) is required but not found.])]) + AC_SUBST(YAML_CFLAGS) + AC_SUBST(YAML_LIBS) +]) + +# Configure config base path option (--with-config-base-dir) +AC_ARG_WITH([config-base-dir], + [AS_HELP_STRING([--with-config-base-dir=PATH], + [Base directory for config files (default: /usr/share/qcom)])], + [config_base_dir="$withval"], + [config_base_dir="/usr/share/qcom/"]) +AC_MSG_NOTICE([Config base path: $config_base_dir]) +AC_SUBST([CONFIG_BASE_DIR], ["$config_base_dir"]) + AC_CONFIG_FILES([ Makefile inc/Makefile diff --git a/inc/Makefile.am b/inc/Makefile.am index fa89ed3..dd76127 100644 --- a/inc/Makefile.am +++ b/inc/Makefile.am @@ -40,6 +40,7 @@ noinst_HEADERS = \ fastrpc_cap.h \ fastrpc_common.h \ fastrpc_config.h \ + fastrpc_config_parser.h \ fastrpc_context.h \ fastrpc_hash_table.h \ fastrpc_internal.h \ diff --git a/inc/apps_std_internal.h b/inc/apps_std_internal.h index 74d7ae6..37be0c2 100644 --- a/inc/apps_std_internal.h +++ b/inc/apps_std_internal.h @@ -22,23 +22,6 @@ #define MAX_NON_PRELOAD_LIBS_LEN 2048 #define FILE_EXT ".so" -// Locations where shell file can be found -#ifndef ENABLE_UPSTREAM_DRIVER_INTERFACE -#ifndef DSP_MOUNT_LOCATION -#define DSP_MOUNT_LOCATION "/dsp/" -#endif -#ifndef DSP_DOM_LOCATION -#define DSP_DOM_LOCATION "/dsp/xdsp" -#endif -#else /* ENABLE_UPSTREAM_DRIVER_INTERFACE */ -#ifndef DSP_MOUNT_LOCATION -#define DSP_MOUNT_LOCATION "/usr/lib/dsp/" -#endif -#ifndef DSP_DOM_LOCATION -#define DSP_DOM_LOCATION "/usr/lib/dsp/xdspn" -#endif -#endif /* ENABLE_UPSTREAM_DRIVER_INTERFACE */ - #ifndef VENDOR_DSP_LOCATION #define VENDOR_DSP_LOCATION "/vendor/dsp/" #endif @@ -46,14 +29,12 @@ #define VENDOR_DOM_LOCATION "/vendor/dsp/xdsp/" #endif -// Search path used by fastRPC to search skel library, .debugconfig and .farf files -#ifndef DSP_SEARCH_PATH -#define DSP_SEARCH_PATH ";/usr/lib/rfsa/adsp;/vendor/lib/rfsa/adsp;/vendor/dsp/;/usr/lib/dsp/;" -#endif - // Search path used by fastRPC for acdb path #ifndef ADSP_AVS_CFG_PATH #define ADSP_AVS_CFG_PATH ";/etc/acdbdata/;" #endif +int fopen_from_dirlist(const char *dirList, const char *delim, + const char *mode, const char *name, apps_std_FILE *psout); + #endif /*__APPS_STD_INTERNAL_H__*/ diff --git a/inc/fastrpc_config_parser.h b/inc/fastrpc_config_parser.h new file mode 100644 index 0000000..c820b20 --- /dev/null +++ b/inc/fastrpc_config_parser.h @@ -0,0 +1,17 @@ +// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +// SPDX-License-Identifier: BSD-3-Clause-Clear + +#ifndef FASTRPC_YAML_PARSER_H +#define FASTRPC_YAML_PARSER_H + +// DEFAULT_DSP_SEARCH_PATHS intentionally left empty - these paths should be provided through configuration files +#ifndef DEFAULT_DSP_SEARCH_PATHS +#define DEFAULT_DSP_SEARCH_PATHS "" +#endif +#define DSP_LIB_KEY "DSP_LIBRARY_PATH" + +extern char DSP_LIBS_LOCATION[PATH_MAX]; + +void configure_dsp_paths(); + +#endif /*FASTRPC_YAML_PARSER_H*/ \ No newline at end of file diff --git a/inc/fastrpc_internal.h b/inc/fastrpc_internal.h index 859bfe8..31f1b5d 100644 --- a/inc/fastrpc_internal.h +++ b/inc/fastrpc_internal.h @@ -334,7 +334,7 @@ struct handle_list { * @brief API to get the DSP_SEARCH_PATH stored locally as static. * @get_path : get_path will be updated with the path stored in DSP_SEARCH_PATH locally. **/ -void get_default_dsp_search_path(char* path); +const char* get_dsp_search_path(); /** * @brief API to map memory to the remote domain diff --git a/src/Makefile.am b/src/Makefile.am index 7c28c49..9a3608d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,6 +1,12 @@ lib_LTLIBRARIES = -LIBDSPRPC_CFLAGS = -fno-short-enums -U_DEBUG -DARM_ARCH_7A -DLE_ENABLE -DENABLE_UPSTREAM_DRIVER_INTERFACE -DUSE_SYSLOG -I$(top_srcdir)/inc +LIBDSPRPC_CFLAGS = -fno-short-enums -U_DEBUG -DARM_ARCH_7A -DLE_ENABLE -DENABLE_UPSTREAM_DRIVER_INTERFACE -DUSE_SYSLOG -DCONFIG_BASE_DIR='"$(CONFIG_BASE_DIR)"' -DMACHINE_NAME_PATH='"/sys/firmware/devicetree/base/model"' -I$(top_srcdir)/inc + +if ANDROID_CC +LIBDSPRPC_CFLAGS += -DDEFAULT_DSP_SEARCH_PATHS='";/vendor/lib/rfsa/adsp;/vendor/dsp;"' +else +LIBDSPRPC_CFLAGS += -DPARSE_YAML @YAML_CFLAGS@ -DDEFAULT_DSP_SEARCH_PATHS='";/usr/lib/rfsa/adsp;/usr/lib/dsp;"' +endif LIBDSPRPC_SOURCES = \ fastrpc_apps_user.c \ @@ -48,6 +54,12 @@ LIBDSPRPC_SOURCES = \ mod_table.c \ fastrpc_context.c +if ANDROID_CC +# Do nothing (or Android-specific sources) +else +LIBDSPRPC_SOURCES += fastrpc_config_parser.c +endif + LIBDEFAULT_LISTENER_SOURCES = \ adsp_default_listener.c \ adsp_default_listener_stub.c \ @@ -55,6 +67,12 @@ LIBDEFAULT_LISTENER_SOURCES = \ if ANDROID_CC USE_LOG = -llog +else +# Add YAML libs to link flags for non-Android builds +libadsprpc_la_LIBADD = @YAML_LIBS@ +libcdsprpc_la_LIBADD = @YAML_LIBS@ +libsdsprpc_la_LIBADD = @YAML_LIBS@ +libgdsprpc_la_LIBADD = @YAML_LIBS@ endif ADSP_CFLAGS = $(LIBDSPRPC_CFLAGS) -DDEFAULT_DOMAIN_ID=0 diff --git a/src/apps_std_imp.c b/src/apps_std_imp.c index 3458d8c..0027b35 100644 --- a/src/apps_std_imp.c +++ b/src/apps_std_imp.c @@ -932,11 +932,15 @@ static int get_dirlist_from_env(const char *envvarname, char **ppDirList) { char *dirList = NULL; char *dirListBuf = NULL; char *srcStr = NULL; + const char *dsp_search_path = NULL; int nErr = AEE_SUCCESS; int envListLen = 0; int envListPrependLen = 0; int listLen = 0; - int envLenGuess = STD_MAX(ENV_LEN_GUESS, 1 + strlen(DSP_SEARCH_PATH)); + int envLenGuess = 0; + + dsp_search_path = get_dsp_search_path(); + envLenGuess = STD_MAX(ENV_LEN_GUESS, 1 + strlen(dsp_search_path)); FARF(RUNTIME_RPC_LOW, "Entering %s", __func__); VERIFYC(NULL != ppDirList, AEE_ERPC); @@ -950,8 +954,8 @@ static int get_dirlist_from_env(const char *envvarname, char **ppDirList) { strlen(ADSP_LIBRARY_PATH)) == 0 || strncmp(envvarname, DSP_LIBRARY_PATH, strlen(DSP_LIBRARY_PATH)) == 0) { - // Calculate total length of env and DSP_SEARCH_PATH - envListPrependLen = envListLen + strlen(DSP_SEARCH_PATH); + // Calculate total length of env + semicolon + DSP_SEARCH_PATH + envListPrependLen = envListLen + 1 + strlen(dsp_search_path); if (envLenGuess < envListPrependLen) { FREEIF(envListBuf); VERIFYC(envListBuf = @@ -961,8 +965,10 @@ static int get_dirlist_from_env(const char *envvarname, char **ppDirList) { VERIFY(0 == (nErr = apps_std_getenv(envvarname, envList, envListPrependLen, &listLen))); } + // Append semicolon before DSP_SEARCH_PATH + strlcat(envList, ";", envListPrependLen); // Append default DSP_SEARCH_PATH to user defined env - strlcat(envList, DSP_SEARCH_PATH, envListPrependLen); + strlcat(envList, dsp_search_path, envListPrependLen); envListLen = envListPrependLen; } else if (strncmp(envvarname, ADSP_AVS_PATH, strlen(ADSP_AVS_PATH)) == 0) { @@ -986,7 +992,7 @@ static int get_dirlist_from_env(const char *envvarname, char **ppDirList) { strncmp(envvarname, DSP_LIBRARY_PATH, strlen(DSP_LIBRARY_PATH)) == 0) { envListLen = listLen = - 1 + strlcpy(envListBuf, DSP_SEARCH_PATH, envLenGuess); + 1 + strlcpy(envListBuf, dsp_search_path, envLenGuess); } else if (strncmp(envvarname, ADSP_AVS_PATH, strlen(ADSP_AVS_PATH)) == 0) { envListLen = listLen = @@ -1018,42 +1024,14 @@ static int get_dirlist_from_env(const char *envvarname, char **ppDirList) { return nErr; } -__QAIC_IMPL_EXPORT int __QAIC_IMPL(apps_std_fopen_with_env)( - const char *envvarname, const char *delim, const char *name, - const char *mode, apps_std_FILE *psout) __QAIC_IMPL_ATTRIBUTE { - +int fopen_from_dirlist(const char *dirList, const char *delim, + const char *mode, const char *name, apps_std_FILE *psout) { int nErr = AEE_SUCCESS; - char *dirName = NULL; - char *pos = NULL; - char *dirListBuf = NULL; - char *dirList = NULL; - char *absName = NULL; - const char *envVar = NULL; + char *absName = NULL, *dirName = NULL, *pos = NULL; uint16_t absNameLen = 0; int domain = GET_DOMAIN_FROM_EFFEC_DOMAIN_ID(get_current_domain()); - FARF(RUNTIME_RPC_LOW, "Entering %s", __func__); - VERIFYC(NULL != mode, AEE_EBADPARM); - VERIFYC(NULL != delim, AEE_EBADPARM); - VERIFYC(NULL != name, AEE_EBADPARM); - VERIFYC(NULL != envvarname, AEE_EBADPARM); - FASTRPC_ATRACE_BEGIN_L("%s for %s in %s mode from path in environment " - "variable %s delimited with %s", - __func__, name, mode, envvarname, delim); - if (strncmp(envvarname, ADSP_LIBRARY_PATH, - strlen(ADSP_LIBRARY_PATH)) == 0) { - if (getenv(DSP_LIBRARY_PATH)) { - envVar = DSP_LIBRARY_PATH; - } else { - envVar = ADSP_LIBRARY_PATH; - } - } else { - envVar = envvarname; - } - - VERIFY(0 == (nErr = get_dirlist_from_env(envVar, &dirListBuf))); - VERIFYC(NULL != (dirList = dirListBuf), AEE_EBADPARM); - FARF(RUNTIME_RPC_HIGH, "%s dirList %s", __func__, dirList); + VERIFYC(NULL != dirList, AEE_EBADPARM); while (dirList) { pos = strstr(dirList, delim); @@ -1084,7 +1062,8 @@ __QAIC_IMPL_EXPORT int __QAIC_IMPL(apps_std_fopen_with_env)( if (AEE_SUCCESS == nErr) { // Success FARF(ALWAYS, "Successfully opened file %s", absName); - goto bail; + FREEIF(absName); + return nErr; } FREEIF(absName); @@ -1109,12 +1088,50 @@ __QAIC_IMPL_EXPORT int __QAIC_IMPL(apps_std_fopen_with_env)( (strncmp(name, TESTSIG_FILE_NAME, strlen(TESTSIG_FILE_NAME)) != 0)) FARF(ALWAYS, "Successfully opened file %s", name); - goto bail; + FREEIF(absName); + return nErr; } - FREEIF(absName); } bail: FREEIF(absName); + return nErr; +} + +__QAIC_IMPL_EXPORT int __QAIC_IMPL(apps_std_fopen_with_env)( + const char *envvarname, const char *delim, const char *name, + const char *mode, apps_std_FILE *psout) __QAIC_IMPL_ATTRIBUTE { + + int nErr = AEE_SUCCESS; + char *dirListBuf = NULL; + char *dirList = NULL; + const char *envVar = NULL; + + FARF(RUNTIME_RPC_LOW, "Entering %s", __func__); + VERIFYC(NULL != mode, AEE_EBADPARM); + VERIFYC(NULL != delim, AEE_EBADPARM); + VERIFYC(NULL != name, AEE_EBADPARM); + VERIFYC(NULL != envvarname, AEE_EBADPARM); + FASTRPC_ATRACE_BEGIN_L("%s for %s in %s mode from path in environment " + "variable %s delimited with %s", + __func__, name, mode, envvarname, delim); + if (strncmp(envvarname, ADSP_LIBRARY_PATH, + strlen(ADSP_LIBRARY_PATH)) == 0) { + if (getenv(DSP_LIBRARY_PATH)) { + envVar = DSP_LIBRARY_PATH; + } else { + envVar = ADSP_LIBRARY_PATH; + } + } else { + envVar = envvarname; + } + + VERIFY(0 == (nErr = get_dirlist_from_env(envVar, &dirListBuf))); + VERIFYC(NULL != (dirList = dirListBuf), AEE_EBADPARM); + FARF(RUNTIME_RPC_HIGH, "%s dirList %s", __func__, dirList); + + nErr = fopen_from_dirlist(dirList, delim, mode, name, psout); + +bail: FREEIF(dirListBuf); if (nErr != AEE_SUCCESS) { if (ERRNO != ENOENT || diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index 9668c06..d3bc321 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -75,17 +75,13 @@ #include "fastrpc_context.h" #include "fastrpc_process_attributes.h" #include "fastrpc_trace.h" +#include "fastrpc_config_parser.h" -#ifndef ENABLE_UPSTREAM_DRIVER_INTERFACE -#define DSP_MOUNT_LOCATION "/dsp/" -#define DSP_DOM_LOCATION "/dsp/xdsp" -#else -#define DSP_MOUNT_LOCATION "/usr/lib/dsp/" -#define DSP_DOM_LOCATION "/usr/lib/dsp/xdspn" -#endif #define VENDOR_DSP_LOCATION "/vendor/dsp/" #define VENDOR_DOM_LOCATION "/vendor/dsp/xdsp/" +char DSP_LIBS_LOCATION[PATH_MAX] = DEFAULT_DSP_SEARCH_PATHS; + #ifdef LE_ENABLE #define PROPERTY_VALUE_MAX \ 92 // as this macro is defined in cutils for Android platforms, defined @@ -3437,8 +3433,8 @@ static int open_shell(int domain_id, apps_std_FILE *fh, int unsigned_shell) { char *absName = NULL; char *shell_absName = NULL; char *domain_str = NULL; + char dir_list[PATH_MAX] = {0}; uint16_t shell_absNameLen = 0, absNameLen = 0; - ; int nErr = AEE_SUCCESS; int domain = GET_DOMAIN_FROM_EFFEC_DOMAIN_ID(domain_id); const char *shell_name = SIGNED_SHELL; @@ -3459,27 +3455,13 @@ static int open_shell(int domain_id, apps_std_FILE *fh, int unsigned_shell) { (shell_absName = (char *)malloc(sizeof(char) * shell_absNameLen)), AEE_ENOMEMORY); strlcpy(shell_absName, shell_name, shell_absNameLen); - strlcat(shell_absName, domain_str, shell_absNameLen); - absNameLen = strlen(DSP_MOUNT_LOCATION) + shell_absNameLen + 1; - VERIFYC(NULL != (absName = (char *)malloc(sizeof(char) * absNameLen)), - AEE_ENOMEMORY); - strlcpy(absName, DSP_MOUNT_LOCATION, absNameLen); - strlcat(absName, shell_absName, absNameLen); + FARF(ALWAYS, "trying to open shell %s from DSP_LIBS_LOCATION %s", shell_absName, DSP_LIBS_LOCATION); + strlcpy(dir_list, DSP_LIBS_LOCATION, sizeof(dir_list)); + FARF(ALWAYS, "trying to open shell %s from dir_list %s", shell_absName, dir_list); + nErr = fopen_from_dirlist(dir_list, ";", "r", shell_absName, fh); - nErr = apps_std_fopen(absName, "r", fh); - if (nErr) { - absNameLen = strlen(DSP_DOM_LOCATION) + shell_absNameLen + 1; - VERIFYC(NULL != - (absName = (char *)realloc(absName, sizeof(char) * absNameLen)), - AEE_ENOMEMORY); - strlcpy(absName, DSP_MOUNT_LOCATION, absNameLen); - strlcat(absName, SUBSYSTEM_NAME[domain], absNameLen); - strlcat(absName, "/", absNameLen); - strlcat(absName, shell_absName, absNameLen); - nErr = apps_std_fopen(absName, "r", fh); - } if (nErr) { absNameLen = strlen(VENDOR_DSP_LOCATION) + shell_absNameLen + 1; VERIFYC(NULL != @@ -3503,7 +3485,7 @@ static int open_shell(int domain_id, apps_std_FILE *fh, int unsigned_shell) { } } if (!nErr) - FARF(RUNTIME_RPC_HIGH, "Successfully opened %s, domain %d", absName, domain); + FARF(RUNTIME_RPC_HIGH, "Successfully opened %s, domain %d", shell_absName, domain); bail: if (domain_str) { free(domain_str); @@ -3523,10 +3505,9 @@ static int open_shell(int domain_id, apps_std_FILE *fh, int unsigned_shell) { *fh = -1; } else { FARF(ERROR, - "Error 0x%x: %s failed for domain %d search paths used are %s, %s, " - "%s (errno %s)\n", - nErr, __func__, domain, DSP_MOUNT_LOCATION, VENDOR_DSP_LOCATION, - VENDOR_DOM_LOCATION, strerror(errno)); + "Error 0x%x: %s failed for domain %d search paths used are %s " + "(errno %s)\n", + nErr, __func__, domain, DSP_LIBS_LOCATION, strerror(errno)); } } return nErr; @@ -4155,6 +4136,10 @@ static void exit_thread(void *value) { pthread_setspecific(tlsKey, (void *)NULL); } +const char* get_dsp_search_path() { + return DSP_LIBS_LOCATION; +} + /* * Called only once by fastrpc_init_once * Initializes the data structures @@ -4170,6 +4155,9 @@ static int fastrpc_apps_user_init(void) { VERIFY(AEE_SUCCESS == (nErr = PL_INIT(gpls))); VERIFY(AEE_SUCCESS == (nErr = PL_INIT(rpcmem))); VERIFY(AEE_SUCCESS == (nErr = pthread_key_create(&tlsKey, exit_thread))); +#ifdef PARSE_YAML + configure_dsp_paths(); +#endif fastrpc_mem_init(); fastrpc_context_table_init(); fastrpc_log_init(); diff --git a/src/fastrpc_config_parser.c b/src/fastrpc_config_parser.c new file mode 100644 index 0000000..cdecc94 --- /dev/null +++ b/src/fastrpc_config_parser.c @@ -0,0 +1,181 @@ +// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +// SPDX-License-Identifier: BSD-3-Clause-Clear + +#define _GNU_SOURCE +#ifndef VERIFY_PRINT_WARN +#define VERIFY_PRINT_WARN +#endif // VERIFY_PRINT_WARN +#ifndef VERIFY_PRINT_ERROR_ALWAYS +#define VERIFY_PRINT_ERROR_ALWAYS +#endif // VERIFY_PRINT_ERROR_ALWAYS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FARF_ERROR 1 +#define FARF_HIGH 1 +#define FARF_MED 1 +#define FARF_LOW 1 +#define FARF_CRITICAL 1 // Push log's to all hooks and persistent buffer. + +#define CONFIG_DIR CONFIG_BASE_DIR "/conf.d/" + +#include "AEEQList.h" +#include "AEEStdErr.h" +#include "AEEstd.h" +#include "HAP_farf.h" +#include "apps_std_internal.h" +#include "fastrpc_config_parser.h" + +static int compare_strings(const void *a, const void *b) { + return strcmp(*(const char **)a, *(const char **)b); +} + +static void get_dsp_lib_path(const char *machine_name, const char *filepath, char *dsp_lib_paths) { + yaml_parser_t parser; + yaml_event_t event; + int done = 0; + int in_machines = 0; + int in_target_machine = 0; + int found_dsp_path = 0; + char current_machine[PATH_MAX] = {0}; + + FILE *file = fopen(filepath, "r"); + if (!file) + return; + + if (!yaml_parser_initialize(&parser)) { + FARF(ALWAYS, "Warning: Failed to initialize YAML parser for file %s\n", filepath); + fclose(file); + return; + } + + yaml_parser_set_input_file(&parser, file); + + while (!done) { + if (!yaml_parser_parse(&parser, &event)) { + FARF(ALWAYS, "Warning: YAML parser error in file %s\n", filepath); + break; + } + + switch (event.type) { + case YAML_SCALAR_EVENT: { + const char *value = (const char *)event.data.scalar.value; + + if (!in_machines && strcmp(value, "machines") == 0) { + in_machines = 1; + } else if (in_machines && !in_target_machine) { + // This is a machine name key + strlcpy(current_machine, value, sizeof(current_machine)); + if (strcmp(current_machine, machine_name) == 0) { + in_target_machine = 1; + } + } else if (in_target_machine && strcmp(value, DSP_LIB_KEY) == 0) { + // Next scalar will be the DSP_LIBRARY_PATH value + yaml_event_delete(&event); + if (yaml_parser_parse(&parser, &event) && event.type == YAML_SCALAR_EVENT) { + strlcpy(dsp_lib_paths, (const char *)event.data.scalar.value, PATH_MAX); + FARF(ALWAYS, "dsp_lib_paths is %s", dsp_lib_paths); + found_dsp_path = 1; + done = 1; + } + } + break; + } + case YAML_MAPPING_END_EVENT: + if (in_target_machine) { + // Exiting the target machine mapping + in_target_machine = 0; + if (found_dsp_path) { + done = 1; + } + } + break; + case YAML_STREAM_END_EVENT: + done = 1; + break; + default: + break; + } + + yaml_event_delete(&event); + } + + yaml_parser_delete(&parser); + fclose(file); + + if (!found_dsp_path) { + FARF(ALWAYS, "Warning: DSP_LIBRARY_PATH not found for machine [%s] in configuration file %s\n", + machine_name, filepath); + } +} + +static void parse_config_dir(char *machine_name) { + DIR *dir = opendir(CONFIG_DIR); + struct dirent *entry; + char *file_list[1024]; + int file_count = 0; + char dsp_lib_paths[PATH_MAX] = {0}; + + if (!dir) + return; + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_type == DT_REG && + (strstr(entry->d_name, ".yaml") || strstr(entry->d_name, ".yml"))) { + file_list[file_count] = strdup(entry->d_name); + file_count++; + } + } + closedir(dir); + + qsort(file_list, file_count, sizeof(char *), compare_strings); + + for (int i = 0; i < file_count; i++) { + char filepath[PATH_MAX]; + snprintf(filepath, sizeof(filepath), "%s%s", CONFIG_DIR, file_list[i]); + get_dsp_lib_path(machine_name, filepath, dsp_lib_paths); + free(file_list[i]); + } + + if (dsp_lib_paths[0] != '\0') { + strlcpy(DSP_LIBS_LOCATION, CONFIG_BASE_DIR, sizeof(DSP_LIBS_LOCATION)); + //append slash in case user passed config base dir doesn't end with slash '/' + strlcat(DSP_LIBS_LOCATION, "/", sizeof(DSP_LIBS_LOCATION)); + strlcat(DSP_LIBS_LOCATION, dsp_lib_paths, sizeof(DSP_LIBS_LOCATION)); + strlcat(DSP_LIBS_LOCATION, DEFAULT_DSP_SEARCH_PATHS, sizeof(DSP_LIBS_LOCATION)); + } else { + FARF(ALWAYS, "Warning: No DSP library path found for machine [%s] in any configuration file\n", + machine_name); + } +} + +void configure_dsp_paths() { + char machine_name[PATH_MAX] = {0}; + FILE *file = fopen(MACHINE_NAME_PATH, "r"); + + if (file == NULL) + return; + + if (fgets(machine_name, sizeof(machine_name), file) != NULL) + // Remove trailing newline if present + machine_name[strcspn(machine_name, "\n")] = '\0'; + + fclose(file); + parse_config_dir(machine_name); +} \ No newline at end of file diff --git a/src/log_config.c b/src/log_config.c index 0c3ee42..b3f9e1d 100644 --- a/src/log_config.c +++ b/src/log_config.c @@ -392,8 +392,10 @@ static void *file_watcher_thread(void *arg) { remote_handle64 handle; int file_found = 0; char *data_paths = NULL; + const char *dsp_search_path = NULL; FARF(ALWAYS, "%s starting for domain %d\n", __func__, dom); + dsp_search_path = get_dsp_search_path(); // Check for the presence of the .farf file at bootup for (i = 0; i < (int)log_config_watcher[dom].numPaths; ++i) { if (0 == readLogConfigFromPath(dom, log_config_watcher[dom].paths[i].data, @@ -414,9 +416,8 @@ static void *file_watcher_thread(void *arg) { ret = apps_std_getenv(DSP_LIBRARY_PATH, data_paths, ENV_PATH_LEN, &env_list_len); errno = current_errno; - // User has not set the env variable. Get default search paths. if (ret != 0) - memmove(data_paths, DSP_SEARCH_PATH, strlen(DSP_SEARCH_PATH)); + strlcpy(data_paths, dsp_search_path, ENV_PATH_LEN); VERIFY_WPRINTF("%s: Couldn't find file %s, errno (%s) at %s\n", __func__, log_config_watcher[dom].fileToWatch, strerror(errno), data_paths); From 9d933b0e89d9a4336ec64fd8d4c04b79d0b609b7 Mon Sep 17 00:00:00 2001 From: Abhinav Parihar Date: Fri, 12 Sep 2025 10:01:15 +0530 Subject: [PATCH 31/40] Fix envlistlen overwrite when fetching from non-DSP library environment paths Avoid overwriting the `envlistlen` variable when fetching paths from environment variables other than ADSP_LIBRARY_PATH or ADSP_AVS_PATH. The variable was being reset using an uninitialized list length, leading to undefined behavior. This change ensures `envlistlen` retains its correct value as populated during path retrieval. Signed-off-by: Abhinav Parihar --- src/apps_std_imp.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/apps_std_imp.c b/src/apps_std_imp.c index 0027b35..d268ed1 100644 --- a/src/apps_std_imp.c +++ b/src/apps_std_imp.c @@ -984,8 +984,6 @@ static int get_dirlist_from_env(const char *envvarname, char **ppDirList) { } strlcat(envList, ADSP_AVS_CFG_PATH, envListPrependLen); envListLen = envListPrependLen; - } else { - envListLen = listLen; } } else if (strncmp(envvarname, ADSP_LIBRARY_PATH, strlen(ADSP_LIBRARY_PATH)) == 0 || From 520324d88da799ad9b07a88ba29ea4094b012a1b Mon Sep 17 00:00:00 2001 From: Abhinav Parihar Date: Wed, 26 Nov 2025 12:34:36 +0530 Subject: [PATCH 32/40] Update build steps to include libyaml-dev and apt-get update Updated step name from "Install auto tools" to "Install auto tools and dependencies" in both build_linux_gnu.yml and build_linux_arm64.yml. Added `apt-get update` and installed `libyaml-dev` along with automake to ensure required dependencies are available for compilation. Signed-off-by: Abhinav Parihar --- .github/workflows/abi-compat.yml | 60 +++++++++++++++++++++++-- .github/workflows/build_linux_arm64.yml | 60 +++++++++++++++++++++---- .github/workflows/build_linux_gnu.yml | 9 +++- .github/workflows/codeql.yml | 50 ++++++++++++++++++--- Docs/schemas/fastrpc-config-schema.yaml | 2 +- inc/fastrpc_config_parser.h | 2 +- src/fastrpc_config_parser.c | 2 +- 7 files changed, 161 insertions(+), 24 deletions(-) diff --git a/.github/workflows/abi-compat.yml b/.github/workflows/abi-compat.yml index 60e3c0a..281ef33 100644 --- a/.github/workflows/abi-compat.yml +++ b/.github/workflows/abi-compat.yml @@ -36,18 +36,69 @@ jobs: # fetch-depth: 0 required for git worktree to access development branch # without full history, worktree creation will fail fetch-depth: 0 - - - name: Install ABI tools & build dependencies + + - name: Configure APT for amd64 + arm64 (ports) and update + shell: bash run: | + set -euxo pipefail + + # Detect Ubuntu codename + CODENAME="$(. /etc/os-release; echo "${VERSION_CODENAME}")" + : "${CODENAME:?Failed to read VERSION_CODENAME from /etc/os-release}" + echo "Detected Ubuntu codename: ${CODENAME}" + + # 1) Enable ARM64 multiarch + sudo dpkg --add-architecture arm64 + + # 2) Overwrite main sources to be amd64-only (archive + security) + sudo tee /etc/apt/sources.list > /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < Date: Sun, 21 Dec 2025 19:03:11 +0530 Subject: [PATCH 33/40] fix: Link test libs to corrected version libcdsprpc.so.1 Link libhap_example.so and libmultithreading.so to the correct version libcdsprpc.so.1 ensure proper runtime behavior. Signed-off-by: Vinayak Katoch --- test/linux/libhap_example.so | Bin 56672 -> 56696 bytes test/linux/libmultithreading.so | Bin 38616 -> 40840 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/linux/libhap_example.so b/test/linux/libhap_example.so index 05f2b2a4c8aa63b2b4b3bb53944c4932137c39c1..446d83ee1e5686346a6e495d95f4f7415d3ef074 100644 GIT binary patch delta 21378 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 literal 56672 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 Date: Tue, 23 Dec 2025 11:05:17 +0530 Subject: [PATCH 34/40] Build domain-correct paths Use SUBSYSTEM_NAME[domain] instead of the hard-coded "adsp" when computing the absolute file name in fopen_from_dirlist(), so paths are constructed for the actual domain in use. This fixes incorrect lookups on multi domain scenarios. Signed-off-by: Abhinav Parihar --- src/apps_std_imp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps_std_imp.c b/src/apps_std_imp.c index d268ed1..3a69345 100644 --- a/src/apps_std_imp.c +++ b/src/apps_std_imp.c @@ -1043,7 +1043,7 @@ int fopen_from_dirlist(const char *dirList, const char *delim, // Append domain to path absNameLen = - strlen(dirName) + strlen(name) + 2 + strlen("adsp") + 1; + strlen(dirName) + strlen(name) + 2 + strlen(SUBSYSTEM_NAME[domain]) + 1; VERIFYC(NULL != (absName = (char *)malloc(sizeof(char) * absNameLen)), AEE_ENOMEMORY); if ('\0' != *dirName) { From 270b59fced83e0b5e1d03285ba4a313395cf893e Mon Sep 17 00:00:00 2001 From: Abhinav Parihar Date: Tue, 23 Dec 2025 11:07:00 +0530 Subject: [PATCH 35/40] Clean up shell open logs In open_shell(), remove redundant ALWAYS logs. Signed-off-by: Abhinav Parihar --- src/fastrpc_apps_user.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fastrpc_apps_user.c b/src/fastrpc_apps_user.c index d3bc321..f111ccf 100644 --- a/src/fastrpc_apps_user.c +++ b/src/fastrpc_apps_user.c @@ -3457,9 +3457,7 @@ static int open_shell(int domain_id, apps_std_FILE *fh, int unsigned_shell) { strlcpy(shell_absName, shell_name, shell_absNameLen); strlcat(shell_absName, domain_str, shell_absNameLen); - FARF(ALWAYS, "trying to open shell %s from DSP_LIBS_LOCATION %s", shell_absName, DSP_LIBS_LOCATION); strlcpy(dir_list, DSP_LIBS_LOCATION, sizeof(dir_list)); - FARF(ALWAYS, "trying to open shell %s from dir_list %s", shell_absName, dir_list); nErr = fopen_from_dirlist(dir_list, ";", "r", shell_absName, fh); if (nErr) { From e91dd39dd6d1c3d3fd70cdcd158c27f1a7a363ba Mon Sep 17 00:00:00 2001 From: GitHub Service Bot Date: Thu, 15 Jan 2026 19:58:21 +0000 Subject: [PATCH 36/40] Update changelog version to 1.0.2-1 and UNRELEASED suite Signed-off-by: GitHub Service Bot --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index c7ed9ce..7ed9f50 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +fastrpc (1.0.2-1) UNRELEASED; urgency=medium + + * New upstream release + + -- GitHub Service Bot Thu, 15 Jan 2026 19:58:21 +0000 + fastrpc (1.0.0-1) unstable; urgency=medium * Initial packaging. (Closes: #1116042) From 67a4fe320e0cca7321e55c45509882ca3a73cda7 Mon Sep 17 00:00:00 2001 From: Robie Basak Date: Tue, 16 Dec 2025 15:15:46 +0000 Subject: [PATCH 37/40] d/p/dlopen-abi-version: drop (fixed upstream) Signed-off-by: Robie Basak --- debian/patches/dlopen-abi-version | 28 ---------------------------- debian/patches/series | 1 - 2 files changed, 29 deletions(-) delete mode 100644 debian/patches/dlopen-abi-version diff --git a/debian/patches/dlopen-abi-version b/debian/patches/dlopen-abi-version deleted file mode 100644 index e841a46..0000000 --- a/debian/patches/dlopen-abi-version +++ /dev/null @@ -1,28 +0,0 @@ -From: Robie Basak -Description: use versioned .so.1 names when calling dlopen() - Since Debian does not ship the .so symlink in its runtime library packages, - they cannot be used at runtime. Explicitly referencing the required ABI version - is the correct way to do this, to avoid problems with any future ABI - incompatilities. -Bug: https://github.com/qualcomm/fastrpc/issues/230 -Last-Update: 2025-10-22 - ---- a/src/dsprpcd.c -+++ b/src/dsprpcd.c -@@ -16,13 +16,13 @@ - #include - - #ifndef ADSP_DEFAULT_LISTENER_NAME --#define ADSP_DEFAULT_LISTENER_NAME "libadsp_default_listener.so" -+#define ADSP_DEFAULT_LISTENER_NAME "libadsp_default_listener.so.1" - #endif - #ifndef CDSP_DEFAULT_LISTENER_NAME --#define CDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so" -+#define CDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so.1" - #endif - #ifndef SDSP_DEFAULT_LISTENER_NAME --#define SDSP_DEFAULT_LISTENER_NAME "libsdsp_default_listener.so" -+#define SDSP_DEFAULT_LISTENER_NAME "libsdsp_default_listener.so.1" - #endif - #ifndef GDSP_DEFAULT_LISTENER_NAME - #define GDSP_DEFAULT_LISTENER_NAME "libcdsp_default_listener.so.1" diff --git a/debian/patches/series b/debian/patches/series index 4be349c..22ec3fe 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -1,2 +1 @@ -dlopen-abi-version open-shell-path From 613e5df4d0457da864f192bd5051a50221f18b03 Mon Sep 17 00:00:00 2001 From: Robie Basak Date: Tue, 16 Dec 2025 15:16:01 +0000 Subject: [PATCH 38/40] d/gbp.conf: set upstream tag pattern Signed-off-by: Robie Basak --- debian/gbp.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/gbp.conf b/debian/gbp.conf index 13bb1a0..17d382d 100644 --- a/debian/gbp.conf +++ b/debian/gbp.conf @@ -1,2 +1,3 @@ [DEFAULT] debian-branch = debian/latest +upstream-tag = upstream/v%(version)s From 9b5c09fd3ddbc6c521dca6dadc68bb0f9a231000 Mon Sep 17 00:00:00 2001 From: Robie Basak Date: Thu, 8 Jan 2026 12:01:08 +0000 Subject: [PATCH 39/40] Implement new upstream search path configuration * d/control: Build-Depend on libyaml-dev and pkgconf * d/rules: --with-config-base-dir=/usr/share/hexagon-dsp * d/p/open-shell-path: drop (no longer required) * Drop d/guess-dsp.sh (no longer required): - d/{a,c}dsprpcd.service: invoke without guess-dsp.sh - d/fastrpc-support.install: do not install guess-dsp.sh - d/guess-dsp.sh: remove - d/copyright: drop guess-dsp.sh licensing information This requires hexagon-dsp-binaries to provide configuration in /usr/share/hexagon-dsp/conf.d/, eg. /usr/share/hexagon-dsp/conf.d/00-hexagon-dsp-binaries.yaml, which will be done in src:hexagon-dsp-binaries. We do not yet know the packaging version where that change will take place so cannot add a versioned Depends easily. As this package isn't in the archive yet, it's not worth coordinating. Signed-off-by: Robie Basak --- debian/adsprpcd.service | 2 +- debian/cdsprpcd.service | 2 +- debian/control | 2 +- debian/copyright | 25 ----- debian/fastrpc-support.install | 1 - debian/guess-dsp.sh | 44 --------- debian/patches/open-shell-path | 169 --------------------------------- debian/patches/series | 1 - debian/rules | 3 + 9 files changed, 6 insertions(+), 243 deletions(-) delete mode 100644 debian/guess-dsp.sh delete mode 100644 debian/patches/open-shell-path delete mode 100644 debian/patches/series diff --git a/debian/adsprpcd.service b/debian/adsprpcd.service index 112ab40..4cda2b2 100644 --- a/debian/adsprpcd.service +++ b/debian/adsprpcd.service @@ -7,4 +7,4 @@ User=fastrpc Group=fastrpc Restart=always Type=exec -ExecStart=/bin/sh -c '. /usr/share/fastrpc/guess-dsp.sh && /usr/sbin/adsprpcd' +ExecStart=/usr/sbin/adsprpcd diff --git a/debian/cdsprpcd.service b/debian/cdsprpcd.service index 0d642f5..4f79c08 100644 --- a/debian/cdsprpcd.service +++ b/debian/cdsprpcd.service @@ -7,4 +7,4 @@ User=fastrpc Group=fastrpc Restart=always Type=exec -ExecStart=/bin/sh -c '. /usr/share/fastrpc/guess-dsp.sh && /usr/sbin/cdsprpcd' +ExecStart=/usr/sbin/cdsprpcd diff --git a/debian/control b/debian/control index 475d482..1605f44 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Maintainer: Robie Basak Standards-Version: 4.6.2 Section: misc Priority: optional -Build-Depends: debhelper-compat (= 13) +Build-Depends: debhelper-compat (= 13), libyaml-dev, pkgconf Homepage: https://github.com/qualcomm/fastrpc Vcs-Git: https://github.com/qualcomm-linux/pkg-fastrpc -b debian/latest Vcs-Browser: https://github.com/qualcomm-linux/pkg-fastrpc diff --git a/debian/copyright b/debian/copyright index d4bfd19..9d452d8 100644 --- a/debian/copyright +++ b/debian/copyright @@ -54,28 +54,3 @@ License: BSD-1-Clause LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Files: debian/guess-dsp.sh -Copyright: Copyright (c) 2021-2024 Linaro -Comment: the source file declares SPDX MIT but doesn't include the license - text. Since this matches the Expat license according to dep5, the Expat - license text is used here. -License: Expat - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - . - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - . - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/debian/fastrpc-support.install b/debian/fastrpc-support.install index ae5290b..45c2284 100644 --- a/debian/fastrpc-support.install +++ b/debian/fastrpc-support.install @@ -1,6 +1,5 @@ debian/adsprpcd.service usr/lib/systemd/system/ debian/cdsprpcd.service usr/lib/systemd/system/ -debian/guess-dsp.sh usr/share/fastrpc/ usr/bin/adsprpcd usr/sbin usr/bin/cdsprpcd usr/sbin usr/bin/sdsprpcd usr/sbin diff --git a/debian/guess-dsp.sh b/debian/guess-dsp.sh deleted file mode 100644 index 1c4a7c3..0000000 --- a/debian/guess-dsp.sh +++ /dev/null @@ -1,44 +0,0 @@ -# -# Copyright (c) 2021-2024 Linaro -# -# SPDX-License-Identifier: MIT -# - -set -e - -modprobe socinfo || true - -if [ -r /sys/firmware/devicetree/base/model ] ; then - MACHINE=`cat /sys/firmware/devicetree/base/model` - case "$MACHINE" in - *DB820c*) - DSP_LIBRARY_PATH=/usr/share/hexagon-dsp/apq8096/Qualcomm/db820c/dsp - ;; - *"Dragonboard 845c"*) - DSP_LIBRARY_PATH=/usr/share/hexagon-dsp/sdm845/Thundercomm/db845c/dsp - ;; - *"Robotics RB1"*) - DSP_LIBRARY_PATH=/usr/share/hexagon-dsp/qcm2290/Thundercomm/RB1/dsp - ;; - *"QRB4210 RB2"*) - DSP_LIBRARY_PATH=/usr/share/hexagon-dsp/qrb4210/Thundercomm/RB2/dsp - ;; - *"Robotics RB5"*) - DSP_LIBRARY_PATH=/usr/share/hexagon-dsp/sm8250/Thundercomm/RB5/dsp - ;; - *"Robotics RB3gen2"*) - DSP_LIBRARY_PATH=/usr/share/hexagon-dsp/qcm6490/Thundercomm/RB3gen2/dsp - ;; - esac -fi - -if [ -z "$DSP_LIBRARY_PATH" -o ! -d "$DSP_LIBRARY_PATH" ] ; then - echo "Could not find support for this DSP" 1>&2 - exit 1 -fi - -export DSP_LIBRARY_PATH - -# compatibility -ADSP_LIBRARY_PATH=$DSP_LIBRARY_PATH -export ADSP_LIBRARY_PATH diff --git a/debian/patches/open-shell-path b/debian/patches/open-shell-path deleted file mode 100644 index f7819aa..0000000 --- a/debian/patches/open-shell-path +++ /dev/null @@ -1,169 +0,0 @@ -Author: Robie Basak -Description: Use env var DSP_LIBRARY_PATH to find libraries - This is set by guess-dsp.sh (currently in packaging) according to the DSP - found on the hardware to the appropriate FHS path as provided by the - hexagon-dsp-binaries package. - . - Updated 2025-10-22: upstream now expects DSP_LIBRARY_PATH to be ';'-separated, - so the function is rewritten to try each of these paths only since Debian only - needs these. ':' is given as a possible separator too, since this is Unix - convention and upstream may change to follow this convention in the future. -Forwarded: no -Last-Update: 2025-10-22 - ---- a/src/fastrpc_apps_user.c -+++ b/src/fastrpc_apps_user.c -@@ -3437,11 +3437,16 @@ - char *absName = NULL; - char *shell_absName = NULL; - char *domain_str = NULL; -- uint16_t shell_absNameLen = 0, absNameLen = 0; -- ; -+ char *lib_path_env = NULL; -+ char *lib_path_env_dup = NULL; -+ char *saveptr = NULL; -+ char *path = NULL; - int nErr = AEE_SUCCESS; - int domain = GET_DOMAIN_FROM_EFFEC_DOMAIN_ID(domain_id); - const char *shell_name = SIGNED_SHELL; -+ uint16_t shell_absNameLen = 0, absNameLen = 0; -+ size_t pathLen = 0; -+ int found = 0; - - if (1 == unsigned_shell) { - shell_name = UNSIGNED_SHELL; -@@ -3450,60 +3455,76 @@ - if (domain == MDSP_DOMAIN_ID) { - return nErr; - } -- VERIFYC(NULL != (domain_str = (char *)malloc(sizeof(domain))), AEE_ENOMEMORY); -- snprintf(domain_str, sizeof(domain), "%d", domain); -+ VERIFYC(NULL != (domain_str = (char *)malloc(16)), AEE_ENOMEMORY); -+ snprintf(domain_str, 16, "%d", domain); - - shell_absNameLen = strlen(shell_name) + strlen(domain_str) + 1; - - VERIFYC(NULL != -- (shell_absName = (char *)malloc(sizeof(char) * shell_absNameLen)), -+ (shell_absName = (char *)malloc(shell_absNameLen)), - AEE_ENOMEMORY); - strlcpy(shell_absName, shell_name, shell_absNameLen); - - strlcat(shell_absName, domain_str, shell_absNameLen); - -- absNameLen = strlen(DSP_MOUNT_LOCATION) + shell_absNameLen + 1; -- VERIFYC(NULL != (absName = (char *)malloc(sizeof(char) * absNameLen)), -- AEE_ENOMEMORY); -- strlcpy(absName, DSP_MOUNT_LOCATION, absNameLen); -- strlcat(absName, shell_absName, absNameLen); -- -- nErr = apps_std_fopen(absName, "r", fh); -- if (nErr) { -- absNameLen = strlen(DSP_DOM_LOCATION) + shell_absNameLen + 1; -- VERIFYC(NULL != -- (absName = (char *)realloc(absName, sizeof(char) * absNameLen)), -- AEE_ENOMEMORY); -- strlcpy(absName, DSP_MOUNT_LOCATION, absNameLen); -- strlcat(absName, SUBSYSTEM_NAME[domain], absNameLen); -- strlcat(absName, "/", absNameLen); -- strlcat(absName, shell_absName, absNameLen); -- nErr = apps_std_fopen(absName, "r", fh); -+ lib_path_env = getenv("DSP_LIBRARY_PATH"); -+ if (!lib_path_env || !*lib_path_env) { -+ nErr = AEE_EBADPARM; -+ goto bail; - } -- if (nErr) { -- absNameLen = strlen(VENDOR_DSP_LOCATION) + shell_absNameLen + 1; -- VERIFYC(NULL != -- (absName = (char *)realloc(absName, sizeof(char) * absNameLen)), -- AEE_ENOMEMORY); -- strlcpy(absName, VENDOR_DSP_LOCATION, absNameLen); -- strlcat(absName, shell_absName, absNameLen); -+ -+ // Defensive copy for strtok_r -+ VERIFYC(NULL != (lib_path_env_dup = strdup(lib_path_env)), AEE_ENOMEMORY); -+ -+ for (path = strtok_r(lib_path_env_dup, ":;", &saveptr); path != NULL; path = strtok_r(NULL, ":;", &saveptr)) { -+ if (!*path) continue; -+ -+ pathLen = strlen(path); -+ absNameLen = pathLen + shell_absNameLen + 2; // +2 for possible '/' and '\0' -+ VERIFYC(NULL != (absName = (char *)malloc(absNameLen)), AEE_ENOMEMORY); -+ -+ snprintf(absName, absNameLen, "%s%s%s", path, (path[pathLen-1] == '/' ? "" : "/"), shell_absName); - - nErr = apps_std_fopen(absName, "r", fh); -- if (nErr) { -- absNameLen = strlen(VENDOR_DOM_LOCATION) + shell_absNameLen + 1; -- VERIFYC(NULL != (absName = -- (char *)realloc(absName, sizeof(char) * absNameLen)), -- AEE_ENOMEMORY); -- strlcpy(absName, VENDOR_DSP_LOCATION, absNameLen); -- strlcat(absName, SUBSYSTEM_NAME[domain], absNameLen); -- strlcat(absName, "/", absNameLen); -- strlcat(absName, shell_absName, absNameLen); -+ if (!nErr) { -+ found = 1; -+ break; -+ } -+ free(absName); -+ absName = NULL; -+ -+ if (SUBSYSTEM_NAME[domain]) { -+ size_t subsysLen = strlen(SUBSYSTEM_NAME[domain]); -+ absNameLen = pathLen + subsysLen + shell_absNameLen + 3; // +3 for two '/' and '\0' -+ VERIFYC(NULL != (absName = (char *)malloc(absNameLen)), AEE_ENOMEMORY); -+ -+ snprintf(absName, absNameLen, "%s%s%s/%s", path, (path[pathLen-1] == '/' ? "" : "/"), SUBSYSTEM_NAME[domain], shell_absName); - - nErr = apps_std_fopen(absName, "r", fh); -+ if (!nErr) { -+ found = 1; -+ break; -+ } -+ free(absName); -+ absName = NULL; - } - } -- if (!nErr) -+ -+ if (found && absName) { - FARF(RUNTIME_RPC_HIGH, "Successfully opened %s, domain %d", absName, domain); -+ nErr = AEE_SUCCESS; -+ } else { -+ nErr = nErr ? nErr : AEE_ENOMEMORY; -+ if (domain == SDSP_DOMAIN_ID && fh != NULL) { -+ nErr = AEE_SUCCESS; -+ *fh = -1; -+ } else { -+ FARF(ERROR, -+ "Error 0x%x: %s failed for domain %d using DSP_LIBRARY_PATH=%s (errno %s)\n", -+ nErr, __func__, domain, lib_path_env ? lib_path_env : "(unset)", strerror(errno)); -+ } -+ } -+ - bail: - if (domain_str) { - free(domain_str); -@@ -3517,17 +3538,9 @@ - free(absName); - absName = NULL; - } -- if (nErr != AEE_SUCCESS) { -- if (domain == SDSP_DOMAIN_ID && fh != NULL) { -- nErr = AEE_SUCCESS; -- *fh = -1; -- } else { -- FARF(ERROR, -- "Error 0x%x: %s failed for domain %d search paths used are %s, %s, " -- "%s (errno %s)\n", -- nErr, __func__, domain, DSP_MOUNT_LOCATION, VENDOR_DSP_LOCATION, -- VENDOR_DOM_LOCATION, strerror(errno)); -- } -+ if (lib_path_env_dup) { -+ free(lib_path_env_dup); -+ lib_path_env_dup = NULL; - } - return nErr; - } diff --git a/debian/patches/series b/debian/patches/series deleted file mode 100644 index 22ec3fe..0000000 --- a/debian/patches/series +++ /dev/null @@ -1 +0,0 @@ -open-shell-path diff --git a/debian/rules b/debian/rules index b9ab01d..82b20f8 100755 --- a/debian/rules +++ b/debian/rules @@ -3,6 +3,9 @@ %: dh $@ --with autoreconf +override_dh_auto_configure: + dh_auto_configure -- --with-config-base-dir=/usr/share/hexagon-dsp + override_dh_installsystemd: dh_installsystemd -p fastrpc-support adsprpcd.service cdsprpcd.service From 256e3700d0a48ba0d51b8c4d03e53570eaea9e23 Mon Sep 17 00:00:00 2001 From: Robie Basak Date: Tue, 13 Jan 2026 13:31:32 +0000 Subject: [PATCH 40/40] d/changelog: entries for 1.0.2-1 Signed-off-by: Robie Basak --- debian/changelog | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 7ed9f50..9401287 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,19 @@ fastrpc (1.0.2-1) UNRELEASED; urgency=medium * New upstream release + * d/p/dlopen-abi-version: drop (fixed upstream). + * d/gbp.conf: set upstream tag pattern. + * Implement new upstream search path configuration: + - d/control: Build-Depend on libyaml-dev and pkgconf + - d/rules: --with-config-base-dir=/usr/share/hexagon-dsp + - d/p/open-shell-path: drop (no longer required) + - Drop d/guess-dsp.sh (no longer required): + + d/{a,c}dsprpcd.service: invoke without guess-dsp.sh + + d/fastrpc-support.install: do not install guess-dsp.sh + + d/guess-dsp.sh: remove + + d/copyright: drop guess-dsp.sh licensing information - -- GitHub Service Bot Thu, 15 Jan 2026 19:58:21 +0000 + -- Robie Basak Thu, 15 Jan 2026 19:58:21 +0000 fastrpc (1.0.0-1) unstable; urgency=medium